From 475eaf2c76d94523ce618b4f7e720ddc856e8dc1 Mon Sep 17 00:00:00 2001 From: Michael Dokolin Date: Tue, 31 Aug 2021 17:59:51 +0200 Subject: [PATCH 01/81] [Expressions] Fix flaky test checking execution duration (#110338) --- src/plugins/expressions/common/execution/execution.test.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/plugins/expressions/common/execution/execution.test.ts b/src/plugins/expressions/common/execution/execution.test.ts index 2e9d4b91908a0..c478977f60764 100644 --- a/src/plugins/expressions/common/execution/execution.test.ts +++ b/src/plugins/expressions/common/execution/execution.test.ts @@ -763,13 +763,15 @@ describe('Execution', () => { }); test('saves duration it took to execute each function', async () => { + const startTime = Date.now(); const execution = createExecution('add val=1 | add val=2 | add val=3', {}, true); execution.start(-1); await execution.result.toPromise(); + const duration = Date.now() - startTime; for (const node of execution.state.get().ast.chain) { expect(typeof node.debug?.duration).toBe('number'); - expect(node.debug?.duration).toBeLessThan(100); + expect(node.debug?.duration).toBeLessThanOrEqual(duration); expect(node.debug?.duration).toBeGreaterThanOrEqual(0); } }); From 3bae4cdc06ab89819d4afe6960562a2e3b2e3551 Mon Sep 17 00:00:00 2001 From: Nathan L Smith Date: Tue, 31 Aug 2021 11:10:54 -0500 Subject: [PATCH 02/81] Add inspector panel for APM routes (#109696) When the observability:enableInspectEsQueries advanced setting is enabled, show an inspector that includes all queries through useFetcher. Remove the callout. --- .../common/adapters/request/index.ts | 2 +- .../adapters/request/request_adapter.ts | 11 +- src/plugins/inspector/common/index.ts | 1 + x-pack/plugins/apm/kibana.json | 1 + .../plugins/apm/public/application/index.tsx | 1 + .../plugins/apm/public/application/uxApp.tsx | 3 +- .../public/components/routing/app_root.tsx | 13 +- .../shared/apm_header_action_menu/index.tsx | 2 + .../inspector_header_link.tsx | 39 ++++ .../public/components/shared/search_bar.tsx | 56 +----- .../context/apm_plugin/apm_plugin_context.tsx | 2 + .../context/inspector/inspector_context.tsx | 82 +++++++++ .../inspector/use_inspector_context.tsx | 13 ++ .../plugins/apm/public/hooks/use_fetcher.tsx | 10 + x-pack/plugins/apm/public/plugin.ts | 9 +- .../public/services/rest/createCallApmApi.ts | 2 +- x-pack/plugins/apm/server/index.ts | 2 +- .../create_es_client/call_async_with_debug.ts | 33 ++-- .../create_es_client/get_inspect_response.ts | 171 ++++++++++++++++++ .../server/routes/register_routes/index.ts | 7 +- x-pack/plugins/apm/server/routes/typings.ts | 9 - x-pack/plugins/apm/tsconfig.json | 1 + x-pack/plugins/apm/typings/common.d.ts | 3 + .../translations/translations/ja-JP.json | 3 - .../translations/translations/zh-CN.json | 3 - .../tests/inspect/inspect.ts | 10 +- 26 files changed, 382 insertions(+), 107 deletions(-) create mode 100644 x-pack/plugins/apm/public/components/shared/apm_header_action_menu/inspector_header_link.tsx create mode 100644 x-pack/plugins/apm/public/context/inspector/inspector_context.tsx create mode 100644 x-pack/plugins/apm/public/context/inspector/use_inspector_context.tsx create mode 100644 x-pack/plugins/apm/server/lib/helpers/create_es_client/get_inspect_response.ts diff --git a/src/plugins/inspector/common/adapters/request/index.ts b/src/plugins/inspector/common/adapters/request/index.ts index 6cee1c0588d73..807f11569ba2c 100644 --- a/src/plugins/inspector/common/adapters/request/index.ts +++ b/src/plugins/inspector/common/adapters/request/index.ts @@ -6,6 +6,6 @@ * Side Public License, v 1. */ -export { RequestStatistic, RequestStatistics, RequestStatus } from './types'; +export { Request, RequestStatistic, RequestStatistics, RequestStatus } from './types'; export { RequestAdapter } from './request_adapter'; export { RequestResponder } from './request_responder'; diff --git a/src/plugins/inspector/common/adapters/request/request_adapter.ts b/src/plugins/inspector/common/adapters/request/request_adapter.ts index 3da528fb3082e..913f16f74b8e2 100644 --- a/src/plugins/inspector/common/adapters/request/request_adapter.ts +++ b/src/plugins/inspector/common/adapters/request/request_adapter.ts @@ -33,14 +33,19 @@ export class RequestAdapter extends EventEmitter { * {@link RequestResponder#error}. * * @param {string} name The name of this request as it should be shown in the UI. - * @param {object} args Additional arguments for the request. + * @param {RequestParams} params Additional arguments for the request. + * @param {number} [startTime] Set an optional start time for the request * @return {RequestResponder} An instance to add information to the request and finish it. */ - public start(name: string, params: RequestParams = {}): RequestResponder { + public start( + name: string, + params: RequestParams = {}, + startTime: number = Date.now() + ): RequestResponder { const req: Request = { ...params, name, - startTime: Date.now(), + startTime, status: RequestStatus.PENDING, id: params.id ?? uuid(), }; diff --git a/src/plugins/inspector/common/index.ts b/src/plugins/inspector/common/index.ts index 224500b6c43aa..e92c9b670475a 100644 --- a/src/plugins/inspector/common/index.ts +++ b/src/plugins/inspector/common/index.ts @@ -8,6 +8,7 @@ export { Adapters, + Request, RequestAdapter, RequestStatistic, RequestStatistics, diff --git a/x-pack/plugins/apm/kibana.json b/x-pack/plugins/apm/kibana.json index 40e724e306bc0..5bc365e35cb2f 100644 --- a/x-pack/plugins/apm/kibana.json +++ b/x-pack/plugins/apm/kibana.json @@ -12,6 +12,7 @@ "embeddable", "features", "infra", + "inspector", "licensing", "observability", "ruleRegistry", diff --git a/x-pack/plugins/apm/public/application/index.tsx b/x-pack/plugins/apm/public/application/index.tsx index a6b0dc61a3260..feb1ff372dc96 100644 --- a/x-pack/plugins/apm/public/application/index.tsx +++ b/x-pack/plugins/apm/public/application/index.tsx @@ -48,6 +48,7 @@ export const renderApp = ({ core: coreStart, plugins: pluginsSetup, data: pluginsStart.data, + inspector: pluginsStart.inspector, observability: pluginsStart.observability, observabilityRuleTypeRegistry, }; diff --git a/x-pack/plugins/apm/public/application/uxApp.tsx b/x-pack/plugins/apm/public/application/uxApp.tsx index 1b36008e5c353..ddcccf45ccab5 100644 --- a/x-pack/plugins/apm/public/application/uxApp.tsx +++ b/x-pack/plugins/apm/public/application/uxApp.tsx @@ -91,7 +91,7 @@ export function UXAppRoot({ core, deps, config, - corePlugins: { embeddable, maps, observability, data }, + corePlugins: { embeddable, inspector, maps, observability, data }, observabilityRuleTypeRegistry, }: { appMountParameters: AppMountParameters; @@ -108,6 +108,7 @@ export function UXAppRoot({ appMountParameters, config, core, + inspector, plugins, observability, observabilityRuleTypeRegistry, diff --git a/x-pack/plugins/apm/public/components/routing/app_root.tsx b/x-pack/plugins/apm/public/components/routing/app_root.tsx index 498d489691e77..c32828eca2f69 100644 --- a/x-pack/plugins/apm/public/components/routing/app_root.tsx +++ b/x-pack/plugins/apm/public/components/routing/app_root.tsx @@ -26,6 +26,7 @@ import { } from '../../context/apm_plugin/apm_plugin_context'; import { useApmPluginContext } from '../../context/apm_plugin/use_apm_plugin_context'; import { BreadcrumbsContextProvider } from '../../context/breadcrumbs/context'; +import { InspectorContextProvider } from '../../context/inspector/inspector_context'; import { LicenseProvider } from '../../context/license/license_context'; import { TimeRangeIdContextProvider } from '../../context/time_range_id/time_range_id_context'; import { UrlParamsProvider } from '../../context/url_params_context/url_params_context'; @@ -62,12 +63,14 @@ export function ApmAppRoot({ - - + + + - - - + + + + diff --git a/x-pack/plugins/apm/public/components/shared/apm_header_action_menu/index.tsx b/x-pack/plugins/apm/public/components/shared/apm_header_action_menu/index.tsx index 7d000a29dcbec..633d03ce8e1df 100644 --- a/x-pack/plugins/apm/public/components/shared/apm_header_action_menu/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/apm_header_action_menu/index.tsx @@ -14,6 +14,7 @@ import { useApmPluginContext } from '../../../context/apm_plugin/use_apm_plugin_ import { AlertingPopoverAndFlyout } from './alerting_popover_flyout'; import { AnomalyDetectionSetupLink } from './anomaly_detection_setup_link'; import { useServiceName } from '../../../hooks/use_service_name'; +import { InspectorHeaderLink } from './inspector_header_link'; export function ApmHeaderActionMenu() { const { core, plugins } = useApmPluginContext(); @@ -65,6 +66,7 @@ export function ApmHeaderActionMenu() { defaultMessage: 'Add data', })} + ); } diff --git a/x-pack/plugins/apm/public/components/shared/apm_header_action_menu/inspector_header_link.tsx b/x-pack/plugins/apm/public/components/shared/apm_header_action_menu/inspector_header_link.tsx new file mode 100644 index 0000000000000..7f1848e76d28a --- /dev/null +++ b/x-pack/plugins/apm/public/components/shared/apm_header_action_menu/inspector_header_link.tsx @@ -0,0 +1,39 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiHeaderLink } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import React from 'react'; +import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; +import { enableInspectEsQueries } from '../../../../../observability/public'; +import { useApmPluginContext } from '../../../context/apm_plugin/use_apm_plugin_context'; +import { useInspectorContext } from '../../../context/inspector/use_inspector_context'; + +export function InspectorHeaderLink() { + const { inspector } = useApmPluginContext(); + const { inspectorAdapters } = useInspectorContext(); + const { + services: { uiSettings }, + } = useKibana(); + const isInspectorEnabled = uiSettings?.get(enableInspectEsQueries); + + const inspect = () => { + inspector.open(inspectorAdapters); + }; + + if (!isInspectorEnabled) { + return null; + } + + return ( + + {i18n.translate('xpack.apm.inspectButtonText', { + defaultMessage: 'Inspect', + })} + + ); +} diff --git a/x-pack/plugins/apm/public/components/shared/search_bar.tsx b/x-pack/plugins/apm/public/components/shared/search_bar.tsx index 6e5896c9b5e4b..55e19e547b282 100644 --- a/x-pack/plugins/apm/public/components/shared/search_bar.tsx +++ b/x-pack/plugins/apm/public/components/shared/search_bar.tsx @@ -7,19 +7,12 @@ import { QueryDslQueryContainer } from '@elastic/elasticsearch/api/types'; import { - EuiCallOut, EuiFlexGroup, + EuiFlexGroupProps, EuiFlexItem, - EuiLink, EuiSpacer, - EuiFlexGroupProps, } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; import React from 'react'; -import { enableInspectEsQueries } from '../../../../observability/public'; -import { useApmPluginContext } from '../../context/apm_plugin/use_apm_plugin_context'; -import { useKibanaUrl } from '../../hooks/useKibanaUrl'; import { useBreakPoints } from '../../hooks/use_break_points'; import { DatePicker } from './DatePicker'; import { KueryBar } from './kuery_bar'; @@ -35,52 +28,6 @@ interface Props { kueryBarBoolFilter?: QueryDslQueryContainer[]; } -function DebugQueryCallout() { - const { uiSettings } = useApmPluginContext().core; - const advancedSettingsUrl = useKibanaUrl('/app/management/kibana/settings', { - query: { - query: 'category:(observability)', - }, - }); - - if (!uiSettings.get(enableInspectEsQueries)) { - return null; - } - - return ( - - - - - {i18n.translate( - 'xpack.apm.searchBar.inspectEsQueriesEnabled.callout.description.advancedSettings', - { defaultMessage: 'Advanced Settings' } - )} - - ), - }} - /> - - - - ); -} - export function SearchBar({ hidden = false, showKueryBar = true, @@ -100,7 +47,6 @@ export function SearchBar({ return ( <> - (result: FetcherResult) => void; + inspectorAdapters: { requests: RequestAdapter }; +} + +const value: InspectorContextValue = { + addInspectorRequest: () => {}, + inspectorAdapters: { requests: new RequestAdapter() }, +}; + +export const InspectorContext = createContext(value); + +export function InspectorContextProvider({ + children, +}: { + children: ReactNode; +}) { + const history = useHistory(); + const { inspectorAdapters } = value; + + function addInspectorRequest( + result: FetcherResult<{ + mainStatisticsData?: { _inspect?: InspectResponse }; + _inspect?: InspectResponse; + }> + ) { + const operations = + result.data?._inspect ?? result.data?.mainStatisticsData?._inspect ?? []; + + operations.forEach((operation) => { + if (operation.response) { + const { id, name } = operation; + const requestParams = { id, name }; + + const requestResponder = inspectorAdapters.requests.start( + id, + requestParams, + operation.startTime + ); + + requestResponder.json(operation.json as object); + + if (operation.stats) { + requestResponder.stats(operation.stats); + } + + requestResponder.finish(operation.status, operation.response); + } + }); + } + + useEffect(() => { + const unregisterCallback = history.listen((newLocation) => { + if (history.location.pathname !== newLocation.pathname) { + inspectorAdapters.requests.reset(); + } + }); + + return () => { + unregisterCallback(); + }; + }, [history, inspectorAdapters]); + + return ( + + {children} + + ); +} diff --git a/x-pack/plugins/apm/public/context/inspector/use_inspector_context.tsx b/x-pack/plugins/apm/public/context/inspector/use_inspector_context.tsx new file mode 100644 index 0000000000000..a60ed6c8c72e1 --- /dev/null +++ b/x-pack/plugins/apm/public/context/inspector/use_inspector_context.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useContext } from 'react'; +import { InspectorContext } from './inspector_context'; + +export function useInspectorContext() { + return useContext(InspectorContext); +} diff --git a/x-pack/plugins/apm/public/hooks/use_fetcher.tsx b/x-pack/plugins/apm/public/hooks/use_fetcher.tsx index df7487290848a..d5a10a6e91539 100644 --- a/x-pack/plugins/apm/public/hooks/use_fetcher.tsx +++ b/x-pack/plugins/apm/public/hooks/use_fetcher.tsx @@ -9,6 +9,7 @@ import { i18n } from '@kbn/i18n'; import React, { useEffect, useMemo, useState } from 'react'; import { IHttpFetchError } from 'src/core/public'; import { useKibana } from '../../../../../src/plugins/kibana_react/public'; +import { useInspectorContext } from '../context/inspector/use_inspector_context'; import { useTimeRangeId } from '../context/time_range_id/use_time_range_id'; import { AutoAbortedAPMClient, @@ -77,6 +78,7 @@ export function useFetcher( }); const [counter, setCounter] = useState(0); const { timeRangeId } = useTimeRangeId(); + const { addInspectorRequest } = useInspectorContext(); useEffect(() => { let controller: AbortController = new AbortController(); @@ -165,6 +167,14 @@ export function useFetcher( /* eslint-enable react-hooks/exhaustive-deps */ ]); + useEffect(() => { + if (result.error) { + addInspectorRequest({ ...result, data: result.error.body?.attributes }); + } else { + addInspectorRequest(result); + } + }, [addInspectorRequest, result]); + return useMemo(() => { return { ...result, diff --git a/x-pack/plugins/apm/public/plugin.ts b/x-pack/plugins/apm/public/plugin.ts index c884f228c85d2..a329ad57e2b33 100644 --- a/x-pack/plugins/apm/public/plugin.ts +++ b/x-pack/plugins/apm/public/plugin.ts @@ -23,13 +23,14 @@ import type { DataPublicPluginStart, } from '../../../../src/plugins/data/public'; import type { EmbeddableStart } from '../../../../src/plugins/embeddable/public'; -import type { FleetStart } from '../../fleet/public'; import type { HomePublicPluginSetup } from '../../../../src/plugins/home/public'; +import { Start as InspectorPluginStart } from '../../../../src/plugins/inspector/public'; import type { PluginSetupContract as AlertingPluginPublicSetup, PluginStartContract as AlertingPluginPublicStart, } from '../../alerting/public'; import type { FeaturesPluginSetup } from '../../features/public'; +import type { FleetStart } from '../../fleet/public'; import type { LicensingPluginSetup } from '../../licensing/public'; import type { MapsStartApi } from '../../maps/public'; import type { MlPluginSetup, MlPluginStart } from '../../ml/public'; @@ -45,15 +46,14 @@ import type { TriggersAndActionsUIPublicPluginStart, } from '../../triggers_actions_ui/public'; import { registerApmAlerts } from './components/alerting/register_apm_alerts'; -import { featureCatalogueEntry } from './featureCatalogueEntry'; import { getApmEnrollmentFlyoutData, LazyApmCustomAssetsExtension, } from './components/fleet_integration'; +import { getLazyApmAgentsTabExtension } from './components/fleet_integration/lazy_apm_agents_tab_extension'; import { getLazyAPMPolicyCreateExtension } from './components/fleet_integration/lazy_apm_policy_create_extension'; import { getLazyAPMPolicyEditExtension } from './components/fleet_integration/lazy_apm_policy_edit_extension'; -import { getLazyApmAgentsTabExtension } from './components/fleet_integration/lazy_apm_agents_tab_extension'; - +import { featureCatalogueEntry } from './featureCatalogueEntry'; export type ApmPluginSetup = ReturnType; export type ApmPluginStart = void; @@ -74,6 +74,7 @@ export interface ApmPluginStartDeps { data: DataPublicPluginStart; embeddable: EmbeddableStart; home: void; + inspector: InspectorPluginStart; licensing: void; maps?: MapsStartApi; ml?: MlPluginStart; diff --git a/x-pack/plugins/apm/public/services/rest/createCallApmApi.ts b/x-pack/plugins/apm/public/services/rest/createCallApmApi.ts index 217d7e050369d..35dbca1b0c955 100644 --- a/x-pack/plugins/apm/public/services/rest/createCallApmApi.ts +++ b/x-pack/plugins/apm/public/services/rest/createCallApmApi.ts @@ -25,10 +25,10 @@ import { FetchOptions } from '../../../common/fetch_options'; import { callApi } from './callApi'; import type { APMServerRouteRepository, - InspectResponse, APMRouteHandlerResources, // eslint-disable-next-line @kbn/eslint/no-restricted-paths } from '../../../server'; +import { InspectResponse } from '../../../typings/common'; export type APMClientOptions = Omit< FetchOptions, diff --git a/x-pack/plugins/apm/server/index.ts b/x-pack/plugins/apm/server/index.ts index b6dd22c528e99..5b97173601950 100644 --- a/x-pack/plugins/apm/server/index.ts +++ b/x-pack/plugins/apm/server/index.ts @@ -131,6 +131,6 @@ export { APM_SERVER_FEATURE_ID } from '../common/alert_types'; export { APMPlugin } from './plugin'; export { APMPluginSetup } from './types'; export { APMServerRouteRepository } from './routes/get_global_apm_server_route_repository'; -export { InspectResponse, APMRouteHandlerResources } from './routes/typings'; +export { APMRouteHandlerResources } from './routes/typings'; export type { ProcessorEvent } from '../common/processor_event'; diff --git a/x-pack/plugins/apm/server/lib/helpers/create_es_client/call_async_with_debug.ts b/x-pack/plugins/apm/server/lib/helpers/create_es_client/call_async_with_debug.ts index 644416e41b1a6..b58a11f637c21 100644 --- a/x-pack/plugins/apm/server/lib/helpers/create_es_client/call_async_with_debug.ts +++ b/x-pack/plugins/apm/server/lib/helpers/create_es_client/call_async_with_debug.ts @@ -7,10 +7,12 @@ /* eslint-disable no-console */ -import { omit } from 'lodash'; import chalk from 'chalk'; import { KibanaRequest } from '../../../../../../../src/core/server'; +import { RequestStatus } from '../../../../../../../src/plugins/inspector'; +import { WrappedElasticsearchClientError } from '../../../../../observability/server'; import { inspectableEsQueriesMap } from '../../../routes/register_routes'; +import { getInspectResponse } from './get_inspect_response'; function formatObj(obj: Record) { return JSON.stringify(obj, null, 2); @@ -39,20 +41,24 @@ export async function callAsyncWithDebug({ return cb(); } - const startTime = process.hrtime(); + const hrStartTime = process.hrtime(); + const startTime = Date.now(); let res: any; - let esError = null; + let esError: WrappedElasticsearchClientError | null = null; + let esRequestStatus: RequestStatus = RequestStatus.PENDING; try { res = await cb(); + esRequestStatus = RequestStatus.OK; } catch (e) { // catch error and throw after outputting debug info esError = e; + esRequestStatus = RequestStatus.ERROR; } if (debug) { const highlightColor = esError ? 'bgRed' : 'inverse'; - const diff = process.hrtime(startTime); + const diff = process.hrtime(hrStartTime); const duration = Math.round(diff[0] * 1000 + diff[1] / 1e6); // duration in ms const { title, body } = getDebugMessage(); @@ -66,14 +72,17 @@ export async function callAsyncWithDebug({ const inspectableEsQueries = inspectableEsQueriesMap.get(request); if (!isCalledWithInternalUser && inspectableEsQueries) { - inspectableEsQueries.push({ - operationName, - response: res, - duration, - requestType, - requestParams: omit(requestParams, 'headers'), - esError: esError?.response ?? esError?.message, - }); + inspectableEsQueries.push( + getInspectResponse({ + esError, + esRequestParams: requestParams, + esRequestStatus, + esResponse: res, + kibanaRequest: request, + operationName, + startTime, + }) + ); } } diff --git a/x-pack/plugins/apm/server/lib/helpers/create_es_client/get_inspect_response.ts b/x-pack/plugins/apm/server/lib/helpers/create_es_client/get_inspect_response.ts new file mode 100644 index 0000000000000..ae91daf9d2e0d --- /dev/null +++ b/x-pack/plugins/apm/server/lib/helpers/create_es_client/get_inspect_response.ts @@ -0,0 +1,171 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; +import type { KibanaRequest } from '../../../../../../../src/core/server'; +import type { + RequestStatistics, + RequestStatus, +} from '../../../../../../../src/plugins/inspector'; +import { WrappedElasticsearchClientError } from '../../../../../observability/server'; +import type { InspectResponse } from '../../../../typings/common'; + +/** + * Get statistics to show on inspector tab. + * + * If you're using searchSource (which we're not), this gets populated from + * https://github.com/elastic/kibana/blob/c7d742cb8b8935f3812707a747a139806e4be203/src/plugins/data/common/search/search_source/inspect/inspector_stats.ts + * + * We do most of the same here, but not using searchSource. + */ +function getStats({ + esRequestParams, + esResponse, + kibanaRequest, +}: { + esRequestParams: Record; + esResponse: any; + kibanaRequest: KibanaRequest; +}) { + const stats: RequestStatistics = { + kibanaApiQueryParameters: { + label: i18n.translate( + 'xpack.apm.inspector.stats.kibanaApiQueryParametersLabel', + { + defaultMessage: 'Kibana API query parameters', + } + ), + description: i18n.translate( + 'xpack.apm.inspector.stats.kibanaApiQueryParametersDescription', + { + defaultMessage: + 'The query parameters used in the Kibana API request that initiated the Elasticsearch request.', + } + ), + value: JSON.stringify(kibanaRequest.query, null, 2), + }, + kibanaApiRoute: { + label: i18n.translate('xpack.apm.inspector.stats.kibanaApiRouteLabel', { + defaultMessage: 'Kibana API route', + }), + description: i18n.translate( + 'xpack.apm.inspector.stats.kibanaApiRouteDescription', + { + defaultMessage: + 'The route of the Kibana API request that initiated the Elasticsearch request.', + } + ), + value: `${kibanaRequest.route.method.toUpperCase()} ${ + kibanaRequest.route.path + }`, + }, + indexPattern: { + label: i18n.translate('xpack.apm.inspector.stats.indexPatternLabel', { + defaultMessage: 'Index pattern', + }), + value: esRequestParams.index, + description: i18n.translate( + 'xpack.apm.inspector.stats.indexPatternDescription', + { + defaultMessage: + 'The index pattern that connected to the Elasticsearch indices.', + } + ), + }, + }; + + if (esResponse?.hits) { + stats.hits = { + label: i18n.translate('xpack.apm.inspector.stats.hitsLabel', { + defaultMessage: 'Hits', + }), + value: `${esResponse.hits.hits.length}`, + description: i18n.translate('xpack.apm.inspector.stats.hitsDescription', { + defaultMessage: 'The number of documents returned by the query.', + }), + }; + } + + if (esResponse?.took) { + stats.queryTime = { + label: i18n.translate('xpack.apm.inspector.stats.queryTimeLabel', { + defaultMessage: 'Query time', + }), + value: i18n.translate('xpack.apm.inspector.stats.queryTimeValue', { + defaultMessage: '{queryTime}ms', + values: { queryTime: esResponse.took }, + }), + description: i18n.translate( + 'xpack.apm.inspector.stats.queryTimeDescription', + { + defaultMessage: + 'The time it took to process the query. ' + + 'Does not include the time to send the request or parse it in the browser.', + } + ), + }; + } + + if (esResponse?.hits?.total !== undefined) { + const total = esResponse.hits.total as { + relation: string; + value: number; + }; + const hitsTotalValue = + total.relation === 'eq' ? `${total.value}` : `> ${total.value}`; + + stats.hitsTotal = { + label: i18n.translate('xpack.apm.inspector.stats.hitsTotalLabel', { + defaultMessage: 'Hits (total)', + }), + value: hitsTotalValue, + description: i18n.translate( + 'xpack.apm.inspector.stats.hitsTotalDescription', + { + defaultMessage: 'The number of documents that match the query.', + } + ), + }; + } + return stats; +} + +/** + * Create a formatted response to be sent in the _inspect key for use in the + * inspector. + */ +export function getInspectResponse({ + esError, + esRequestParams, + esRequestStatus, + esResponse, + kibanaRequest, + operationName, + startTime, +}: { + esError: WrappedElasticsearchClientError | null; + esRequestParams: Record; + esRequestStatus: RequestStatus; + esResponse: any; + kibanaRequest: KibanaRequest; + operationName: string; + startTime: number; +}): InspectResponse[0] { + const id = `${operationName} (${kibanaRequest.route.path})`; + + return { + id, + json: esRequestParams.body, + name: id, + response: { + json: esError ? esError.originalError : esResponse, + }, + startTime, + stats: getStats({ esRequestParams, esResponse, kibanaRequest }), + status: esRequestStatus, + }; +} diff --git a/x-pack/plugins/apm/server/routes/register_routes/index.ts b/x-pack/plugins/apm/server/routes/register_routes/index.ts index 16e77f59f4d02..c660489485505 100644 --- a/x-pack/plugins/apm/server/routes/register_routes/index.ts +++ b/x-pack/plugins/apm/server/routes/register_routes/index.ts @@ -19,12 +19,9 @@ import { } from '@kbn/server-route-repository'; import { mergeRt, jsonRt } from '@kbn/io-ts-utils'; import { pickKeys } from '../../../common/utils/pick_keys'; -import { - APMRouteHandlerResources, - InspectResponse, - TelemetryUsageCounter, -} from '../typings'; +import { APMRouteHandlerResources, TelemetryUsageCounter } from '../typings'; import type { ApmPluginRequestHandlerContext } from '../typings'; +import { InspectResponse } from '../../../typings/common'; const inspectRt = t.exact( t.partial({ diff --git a/x-pack/plugins/apm/server/routes/typings.ts b/x-pack/plugins/apm/server/routes/typings.ts index 76f19a6a0ca3e..6cb43fe64ba70 100644 --- a/x-pack/plugins/apm/server/routes/typings.ts +++ b/x-pack/plugins/apm/server/routes/typings.ts @@ -26,15 +26,6 @@ export interface ApmPluginRequestHandlerContext extends RequestHandlerContext { rac: RacApiRequestHandlerContext; } -export type InspectResponse = Array<{ - response: any; - duration: number; - requestType: string; - requestParams: Record; - esError: Error; - operationName: string; -}>; - export interface APMRouteCreateOptions { options: { tags: Array< diff --git a/x-pack/plugins/apm/tsconfig.json b/x-pack/plugins/apm/tsconfig.json index 6eaf1a3bf1833..c1030d2a4be1d 100644 --- a/x-pack/plugins/apm/tsconfig.json +++ b/x-pack/plugins/apm/tsconfig.json @@ -24,6 +24,7 @@ { "path": "../../../src/plugins/embeddable/tsconfig.json" }, { "path": "../../../src/plugins/home/tsconfig.json" }, { "path": "../../../src/plugins/index_pattern_management/tsconfig.json" }, + { "path": "../../../src/plugins/inspector/tsconfig.json" }, { "path": "../../../src/plugins/kibana_react/tsconfig.json" }, { "path": "../../../src/plugins/kibana_utils/tsconfig.json" }, { "path": "../../../src/plugins/usage_collection/tsconfig.json" }, diff --git a/x-pack/plugins/apm/typings/common.d.ts b/x-pack/plugins/apm/typings/common.d.ts index b94eb6cd97b06..4c0b8520924bc 100644 --- a/x-pack/plugins/apm/typings/common.d.ts +++ b/x-pack/plugins/apm/typings/common.d.ts @@ -6,6 +6,7 @@ */ import type { UnwrapPromise } from '@kbn/utility-types'; +import type { Request } from '../../../../src/plugins/inspector/common'; import '../../../typings/rison_node'; import '../../infra/types/eui'; // EUIBasicTable @@ -27,3 +28,5 @@ type AllowUnknownObjectProperties = T extends object export type PromiseValueType> = UnwrapPromise; export type Maybe = T | null | undefined; + +export type InspectResponse = Request[]; diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 1350b9d799a7c..a41e0695a1bd7 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -5580,9 +5580,6 @@ "xpack.apm.rum.visitorBreakdown.operatingSystem": "オペレーティングシステム", "xpack.apm.rum.visitorBreakdownMap.avgPageLoadDuration": "平均ページ読み込み時間", "xpack.apm.rum.visitorBreakdownMap.pageLoadDurationByRegion": "地域別ページ読み込み時間 (平均) ", - "xpack.apm.searchBar.inspectEsQueriesEnabled.callout.description": "ブラウザーの開発者ツールを開き、API応答を確認すると、すべてのElasticsearchクエリを検査できます。この設定はKibanaの{advancedSettingsLink}で無効にでkます", - "xpack.apm.searchBar.inspectEsQueriesEnabled.callout.description.advancedSettings": "高度な設定", - "xpack.apm.searchBar.inspectEsQueriesEnabled.callout.title": "調査可能なESクエリ (`apm:enableInspectEsQueries`) ", "xpack.apm.searchInput.filter": "フィルター...", "xpack.apm.selectPlaceholder": "オプションを選択:", "xpack.apm.serviceDetails.errorsTabLabel": "エラー", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 85889a4094036..8d2e3607d1d32 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -5608,9 +5608,6 @@ "xpack.apm.rum.visitorBreakdown.operatingSystem": "操作系统", "xpack.apm.rum.visitorBreakdownMap.avgPageLoadDuration": "页面加载平均持续时间", "xpack.apm.rum.visitorBreakdownMap.pageLoadDurationByRegion": "按区域列出的页面加载持续时间(平均值)", - "xpack.apm.searchBar.inspectEsQueriesEnabled.callout.description": "现在可以通过打开浏览器的开发工具和查看 API 响应,来检查各个 Elasticsearch 查询。该设置可以在 Kibana 的“{advancedSettingsLink}”中禁用", - "xpack.apm.searchBar.inspectEsQueriesEnabled.callout.description.advancedSettings": "高级设置", - "xpack.apm.searchBar.inspectEsQueriesEnabled.callout.title": "可检查的 ES 查询 (`apm:enableInspectEsQueries`)", "xpack.apm.searchInput.filter": "筛选...", "xpack.apm.selectPlaceholder": "选择选项:", "xpack.apm.serviceDetails.errorsTabLabel": "错误", diff --git a/x-pack/test/apm_api_integration/tests/inspect/inspect.ts b/x-pack/test/apm_api_integration/tests/inspect/inspect.ts index 77ceedaeb68b9..c2a4dfb77d0e6 100644 --- a/x-pack/test/apm_api_integration/tests/inspect/inspect.ts +++ b/x-pack/test/apm_api_integration/tests/inspect/inspect.ts @@ -53,11 +53,13 @@ export default function customLinksTests({ getService }: FtrProviderContext) { // @ts-expect-error expect(Object.keys(body._inspect[0])).to.eql([ - 'operationName', + 'id', + 'json', + 'name', 'response', - 'duration', - 'requestType', - 'requestParams', + 'startTime', + 'stats', + 'status', ]); }); }); From 3f7c461cd5516b525d9cb37ccc3f03e921477ec9 Mon Sep 17 00:00:00 2001 From: Dmitry Tomashevich <39378793+Dmitriynj@users.noreply.github.com> Date: Tue, 31 Aug 2021 19:26:45 +0300 Subject: [PATCH 03/81] [Graph] Deangularize graph app controller (#106587) * [Graph] deaungularize control panel * [Graph] move main graph directive to react * [Graph] refactoring * [Graph] remove redundant memoization, update import * [Graph] fix settings menu, clean up the code * [Graph] fix graph settings * [Graph] code refactoring, fixing control panel render issues * [Graph] fix small mistake * [Graph] rename components * [Graph] fix imports * [Graph] fix graph search and inspect panel * [Graph] remove redundant types * [Graph] fix problem with selection list * [Graph] fix functional test which uses selection list * [Graph] fix unit tests, update types * [Graph] fix types * [Discover] fix url queries * [Graph] fix types * [Graph] add react router, remove angular stuff * [Graph] fix styles * [Graph] fix i18n * [Graph] fix navigation to a new workspace creation * [Graph] fix issues from comments * [Graph] add suggested changed * Update x-pack/plugins/graph/public/components/graph_visualization/graph_visualization.tsx Co-authored-by: Marco Liberati * [Graph] remove brace lib from imports * [Graph] fix url navigation between workspaces, fix types * [Graph] refactoring, fixing url issue * [Graph] update graph dependencies * [Graph] add comments * [Graph] fix types * [Graph] fix new button, fix control panel styles * [Graph] apply suggestions Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Marco Liberati --- x-pack/plugins/graph/public/_main.scss | 1 + .../public/angular/templates/_index.scss | 3 - .../graph/public/angular/templates/index.html | 362 ---------- .../angular/templates/listing_ng_wrapper.html | 13 - x-pack/plugins/graph/public/app.js | 646 ------------------ x-pack/plugins/graph/public/application.ts | 120 ++-- .../listing.tsx => apps/listing_route.tsx} | 98 ++- .../graph/public/apps/workspace_route.tsx | 152 +++++ x-pack/plugins/graph/public/badge.js | 24 - .../templates => components}/_graph.scss | 8 - .../graph/public/components/_index.scss | 3 + .../templates => components}/_inspect.scss | 0 .../templates => components}/_sidebar.scss | 22 +- .../plugins/graph/public/components/app.tsx | 76 --- .../control_panel/control_panel.tsx | 143 ++++ .../control_panel/control_panel_tool_bar.tsx | 230 +++++++ .../control_panel/drill_down_icon_links.tsx | 61 ++ .../components/control_panel/drill_downs.tsx | 55 ++ .../public/components/control_panel/index.ts | 8 + .../control_panel/merge_candidates.tsx | 137 ++++ .../components/control_panel/select_style.tsx | 45 ++ .../control_panel/selected_node_editor.tsx | 100 +++ .../control_panel/selected_node_item.tsx | 63 ++ .../control_panel/selection_tool_bar.tsx | 136 ++++ .../_graph_visualization.scss | 8 + .../graph_visualization.test.tsx | 100 ++- .../graph_visualization.tsx | 69 +- .../graph/public/components/inspect_panel.tsx | 99 +++ .../inspect_panel/inspect_panel.tsx | 109 --- .../public/components/search_bar.test.tsx | 49 +- .../graph/public/components/search_bar.tsx | 44 +- .../settings/advanced_settings_form.tsx | 5 +- .../components/settings/blocklist_form.tsx | 19 +- .../components/settings/settings.test.tsx | 18 +- .../public/components/settings/settings.tsx | 66 +- .../components/settings/url_template_list.tsx | 4 +- .../components/workspace_layout/index.ts | 8 + .../workspace_layout/workspace_layout.tsx | 234 +++++++ .../workspace_top_nav_menu.tsx | 175 +++++ .../graph/public/helpers/as_observable.ts | 16 +- .../public/helpers/saved_workspace_utils.ts | 2 +- .../graph/public/helpers/use_graph_loader.ts | 108 +++ .../public/helpers/use_workspace_loader.ts | 120 ++++ x-pack/plugins/graph/public/index.scss | 1 - x-pack/plugins/graph/public/plugin.ts | 3 +- x-pack/plugins/graph/public/router.tsx | 33 + .../services/persistence/deserialize.test.ts | 2 +- .../services/persistence/serialize.test.ts | 4 +- .../public/services/persistence/serialize.ts | 7 +- .../graph/public/services/save_modal.tsx | 4 +- .../workspace}/graph_client_workspace.d.ts | 0 .../workspace}/graph_client_workspace.js | 6 +- .../workspace}/graph_client_workspace.test.js | 0 .../state_management/advanced_settings.ts | 4 +- .../state_management/datasource.sagas.ts | 4 +- .../graph/public/state_management/fields.ts | 11 +- .../public/state_management/legacy.test.ts | 8 +- .../graph/public/state_management/mocks.ts | 17 +- .../state_management/persistence.test.ts | 24 +- .../public/state_management/persistence.ts | 48 +- .../graph/public/state_management/store.ts | 32 +- .../public/state_management/url_templates.ts | 8 +- .../public/state_management/workspace.ts | 80 ++- .../plugins/graph/public/types/persistence.ts | 6 +- .../graph/public/types/workspace_state.ts | 66 +- 65 files changed, 2521 insertions(+), 1606 deletions(-) delete mode 100644 x-pack/plugins/graph/public/angular/templates/_index.scss delete mode 100644 x-pack/plugins/graph/public/angular/templates/index.html delete mode 100644 x-pack/plugins/graph/public/angular/templates/listing_ng_wrapper.html delete mode 100644 x-pack/plugins/graph/public/app.js rename x-pack/plugins/graph/public/{components/listing.tsx => apps/listing_route.tsx} (64%) create mode 100644 x-pack/plugins/graph/public/apps/workspace_route.tsx delete mode 100644 x-pack/plugins/graph/public/badge.js rename x-pack/plugins/graph/public/{angular/templates => components}/_graph.scss (75%) rename x-pack/plugins/graph/public/{angular/templates => components}/_inspect.scss (100%) rename x-pack/plugins/graph/public/{angular/templates => components}/_sidebar.scss (82%) delete mode 100644 x-pack/plugins/graph/public/components/app.tsx create mode 100644 x-pack/plugins/graph/public/components/control_panel/control_panel.tsx create mode 100644 x-pack/plugins/graph/public/components/control_panel/control_panel_tool_bar.tsx create mode 100644 x-pack/plugins/graph/public/components/control_panel/drill_down_icon_links.tsx create mode 100644 x-pack/plugins/graph/public/components/control_panel/drill_downs.tsx create mode 100644 x-pack/plugins/graph/public/components/control_panel/index.ts create mode 100644 x-pack/plugins/graph/public/components/control_panel/merge_candidates.tsx create mode 100644 x-pack/plugins/graph/public/components/control_panel/select_style.tsx create mode 100644 x-pack/plugins/graph/public/components/control_panel/selected_node_editor.tsx create mode 100644 x-pack/plugins/graph/public/components/control_panel/selected_node_item.tsx create mode 100644 x-pack/plugins/graph/public/components/control_panel/selection_tool_bar.tsx create mode 100644 x-pack/plugins/graph/public/components/inspect_panel.tsx delete mode 100644 x-pack/plugins/graph/public/components/inspect_panel/inspect_panel.tsx create mode 100644 x-pack/plugins/graph/public/components/workspace_layout/index.ts create mode 100644 x-pack/plugins/graph/public/components/workspace_layout/workspace_layout.tsx create mode 100644 x-pack/plugins/graph/public/components/workspace_layout/workspace_top_nav_menu.tsx create mode 100644 x-pack/plugins/graph/public/helpers/use_graph_loader.ts create mode 100644 x-pack/plugins/graph/public/helpers/use_workspace_loader.ts create mode 100644 x-pack/plugins/graph/public/router.tsx rename x-pack/plugins/graph/public/{angular => services/workspace}/graph_client_workspace.d.ts (100%) rename x-pack/plugins/graph/public/{angular => services/workspace}/graph_client_workspace.js (99%) rename x-pack/plugins/graph/public/{angular => services/workspace}/graph_client_workspace.test.js (100%) diff --git a/x-pack/plugins/graph/public/_main.scss b/x-pack/plugins/graph/public/_main.scss index 6b32de32c06d0..22a849b0b2a60 100644 --- a/x-pack/plugins/graph/public/_main.scss +++ b/x-pack/plugins/graph/public/_main.scss @@ -21,6 +21,7 @@ */ .gphNoUserSelect { + padding-right: $euiSizeXS; user-select: none; -webkit-touch-callout: none; -webkit-tap-highlight-color: transparent; diff --git a/x-pack/plugins/graph/public/angular/templates/_index.scss b/x-pack/plugins/graph/public/angular/templates/_index.scss deleted file mode 100644 index 0e603b5c98cbe..0000000000000 --- a/x-pack/plugins/graph/public/angular/templates/_index.scss +++ /dev/null @@ -1,3 +0,0 @@ -@import './graph'; -@import './sidebar'; -@import './inspect'; diff --git a/x-pack/plugins/graph/public/angular/templates/index.html b/x-pack/plugins/graph/public/angular/templates/index.html deleted file mode 100644 index 14c37cab9d9fd..0000000000000 --- a/x-pack/plugins/graph/public/angular/templates/index.html +++ /dev/null @@ -1,362 +0,0 @@ -
- - - - - - - - - -
- -
-
- - - - -
- - -
diff --git a/x-pack/plugins/graph/public/angular/templates/listing_ng_wrapper.html b/x-pack/plugins/graph/public/angular/templates/listing_ng_wrapper.html deleted file mode 100644 index b2363ffbaa641..0000000000000 --- a/x-pack/plugins/graph/public/angular/templates/listing_ng_wrapper.html +++ /dev/null @@ -1,13 +0,0 @@ - diff --git a/x-pack/plugins/graph/public/app.js b/x-pack/plugins/graph/public/app.js deleted file mode 100644 index 13661798cabe6..0000000000000 --- a/x-pack/plugins/graph/public/app.js +++ /dev/null @@ -1,646 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import _ from 'lodash'; -import { i18n } from '@kbn/i18n'; -import React from 'react'; -import { Provider } from 'react-redux'; -import { isColorDark, hexToRgb } from '@elastic/eui'; - -import { toMountPoint } from '../../../../src/plugins/kibana_react/public'; -import { showSaveModal } from '../../../../src/plugins/saved_objects/public'; - -import appTemplate from './angular/templates/index.html'; -import listingTemplate from './angular/templates/listing_ng_wrapper.html'; -import { getReadonlyBadge } from './badge'; - -import { GraphApp } from './components/app'; -import { VennDiagram } from './components/venn_diagram'; -import { Listing } from './components/listing'; -import { Settings } from './components/settings'; -import { GraphVisualization } from './components/graph_visualization'; - -import { createWorkspace } from './angular/graph_client_workspace.js'; -import { getEditUrl, getNewPath, getEditPath, setBreadcrumbs } from './services/url'; -import { createCachedIndexPatternProvider } from './services/index_pattern_cache'; -import { urlTemplateRegex } from './helpers/url_template'; -import { asAngularSyncedObservable } from './helpers/as_observable'; -import { colorChoices } from './helpers/style_choices'; -import { createGraphStore, datasourceSelector, hasFieldsSelector } from './state_management'; -import { formatHttpError } from './helpers/format_http_error'; -import { - findSavedWorkspace, - getSavedWorkspace, - deleteSavedWorkspace, -} from './helpers/saved_workspace_utils'; -import { InspectPanel } from './components/inspect_panel/inspect_panel'; - -export function initGraphApp(angularModule, deps) { - const { - chrome, - toastNotifications, - savedObjectsClient, - indexPatterns, - addBasePath, - getBasePath, - data, - capabilities, - coreStart, - storage, - canEditDrillDownUrls, - graphSavePolicy, - overlays, - savedObjects, - setHeaderActionMenu, - uiSettings, - } = deps; - - const app = angularModule; - - app.directive('vennDiagram', function (reactDirective) { - return reactDirective(VennDiagram); - }); - - app.directive('graphVisualization', function (reactDirective) { - return reactDirective(GraphVisualization); - }); - - app.directive('graphListing', function (reactDirective) { - return reactDirective(Listing, [ - ['coreStart', { watchDepth: 'reference' }], - ['createItem', { watchDepth: 'reference' }], - ['findItems', { watchDepth: 'reference' }], - ['deleteItems', { watchDepth: 'reference' }], - ['editItem', { watchDepth: 'reference' }], - ['getViewUrl', { watchDepth: 'reference' }], - ['listingLimit', { watchDepth: 'reference' }], - ['hideWriteControls', { watchDepth: 'reference' }], - ['capabilities', { watchDepth: 'reference' }], - ['initialFilter', { watchDepth: 'reference' }], - ['initialPageSize', { watchDepth: 'reference' }], - ]); - }); - - app.directive('graphApp', function (reactDirective) { - return reactDirective( - GraphApp, - [ - ['storage', { watchDepth: 'reference' }], - ['isInitialized', { watchDepth: 'reference' }], - ['currentIndexPattern', { watchDepth: 'reference' }], - ['indexPatternProvider', { watchDepth: 'reference' }], - ['isLoading', { watchDepth: 'reference' }], - ['onQuerySubmit', { watchDepth: 'reference' }], - ['initialQuery', { watchDepth: 'reference' }], - ['confirmWipeWorkspace', { watchDepth: 'reference' }], - ['coreStart', { watchDepth: 'reference' }], - ['noIndexPatterns', { watchDepth: 'reference' }], - ['reduxStore', { watchDepth: 'reference' }], - ['pluginDataStart', { watchDepth: 'reference' }], - ], - { restrict: 'A' } - ); - }); - - app.directive('graphVisualization', function (reactDirective) { - return reactDirective(GraphVisualization, undefined, { restrict: 'A' }); - }); - - app.directive('inspectPanel', function (reactDirective) { - return reactDirective( - InspectPanel, - [ - ['showInspect', { watchDepth: 'reference' }], - ['lastRequest', { watchDepth: 'reference' }], - ['lastResponse', { watchDepth: 'reference' }], - ['indexPattern', { watchDepth: 'reference' }], - ['uiSettings', { watchDepth: 'reference' }], - ], - { restrict: 'E' }, - { - uiSettings, - } - ); - }); - - app.config(function ($routeProvider) { - $routeProvider - .when('/home', { - template: listingTemplate, - badge: getReadonlyBadge, - controller: function ($location, $scope) { - $scope.listingLimit = savedObjects.settings.getListingLimit(); - $scope.initialPageSize = savedObjects.settings.getPerPage(); - $scope.create = () => { - $location.url(getNewPath()); - }; - $scope.find = (search) => { - return findSavedWorkspace( - { savedObjectsClient, basePath: coreStart.http.basePath }, - search, - $scope.listingLimit - ); - }; - $scope.editItem = (workspace) => { - $location.url(getEditPath(workspace)); - }; - $scope.getViewUrl = (workspace) => getEditUrl(addBasePath, workspace); - $scope.delete = (workspaces) => - deleteSavedWorkspace( - savedObjectsClient, - workspaces.map(({ id }) => id) - ); - $scope.capabilities = capabilities; - $scope.initialFilter = $location.search().filter || ''; - $scope.coreStart = coreStart; - setBreadcrumbs({ chrome }); - }, - }) - .when('/workspace/:id?', { - template: appTemplate, - badge: getReadonlyBadge, - resolve: { - savedWorkspace: function ($rootScope, $route, $location) { - return $route.current.params.id - ? getSavedWorkspace(savedObjectsClient, $route.current.params.id).catch(function (e) { - toastNotifications.addError(e, { - title: i18n.translate('xpack.graph.missingWorkspaceErrorMessage', { - defaultMessage: "Couldn't load graph with ID", - }), - }); - $rootScope.$eval(() => { - $location.path('/home'); - $location.replace(); - }); - // return promise that never returns to prevent the controller from loading - return new Promise(); - }) - : getSavedWorkspace(savedObjectsClient); - }, - indexPatterns: function () { - return savedObjectsClient - .find({ - type: 'index-pattern', - fields: ['title', 'type'], - perPage: 10000, - }) - .then((response) => response.savedObjects); - }, - GetIndexPatternProvider: function () { - return indexPatterns; - }, - }, - }) - .otherwise({ - redirectTo: '/home', - }); - }); - - //======== Controller for basic UI ================== - app.controller('graphuiPlugin', function ($scope, $route, $location) { - function handleError(err) { - const toastTitle = i18n.translate('xpack.graph.errorToastTitle', { - defaultMessage: 'Graph Error', - description: '"Graph" is a product name and should not be translated.', - }); - if (err instanceof Error) { - toastNotifications.addError(err, { - title: toastTitle, - }); - } else { - toastNotifications.addDanger({ - title: toastTitle, - text: String(err), - }); - } - } - - async function handleHttpError(error) { - toastNotifications.addDanger(formatHttpError(error)); - } - - // Replacement function for graphClientWorkspace's comms so - // that it works with Kibana. - function callNodeProxy(indexName, query, responseHandler) { - const request = { - body: JSON.stringify({ - index: indexName, - query: query, - }), - }; - $scope.loading = true; - return coreStart.http - .post('../api/graph/graphExplore', request) - .then(function (data) { - const response = data.resp; - if (response.timed_out) { - toastNotifications.addWarning( - i18n.translate('xpack.graph.exploreGraph.timedOutWarningText', { - defaultMessage: 'Exploration timed out', - }) - ); - } - responseHandler(response); - }) - .catch(handleHttpError) - .finally(() => { - $scope.loading = false; - $scope.$digest(); - }); - } - - //Helper function for the graphClientWorkspace to perform a query - const callSearchNodeProxy = function (indexName, query, responseHandler) { - const request = { - body: JSON.stringify({ - index: indexName, - body: query, - }), - }; - $scope.loading = true; - coreStart.http - .post('../api/graph/searchProxy', request) - .then(function (data) { - const response = data.resp; - responseHandler(response); - }) - .catch(handleHttpError) - .finally(() => { - $scope.loading = false; - $scope.$digest(); - }); - }; - - $scope.indexPatternProvider = createCachedIndexPatternProvider( - $route.current.locals.GetIndexPatternProvider.get - ); - - const store = createGraphStore({ - basePath: getBasePath(), - addBasePath, - indexPatternProvider: $scope.indexPatternProvider, - indexPatterns: $route.current.locals.indexPatterns, - createWorkspace: (indexPattern, exploreControls) => { - const options = { - indexName: indexPattern, - vertex_fields: [], - // Here we have the opportunity to look up labels for nodes... - nodeLabeller: function () { - // console.log(newNodes); - }, - changeHandler: function () { - //Allows DOM to update with graph layout changes. - $scope.$apply(); - }, - graphExploreProxy: callNodeProxy, - searchProxy: callSearchNodeProxy, - exploreControls, - }; - $scope.workspace = createWorkspace(options); - }, - setLiveResponseFields: (fields) => { - $scope.liveResponseFields = fields; - }, - setUrlTemplates: (urlTemplates) => { - $scope.urlTemplates = urlTemplates; - }, - getWorkspace: () => { - return $scope.workspace; - }, - getSavedWorkspace: () => { - return $route.current.locals.savedWorkspace; - }, - notifications: coreStart.notifications, - http: coreStart.http, - overlays: coreStart.overlays, - savedObjectsClient, - showSaveModal, - setWorkspaceInitialized: () => { - $scope.workspaceInitialized = true; - }, - savePolicy: graphSavePolicy, - changeUrl: (newUrl) => { - $scope.$evalAsync(() => { - $location.url(newUrl); - }); - }, - notifyAngular: () => { - $scope.$digest(); - }, - chrome, - I18nContext: coreStart.i18n.Context, - }); - - // register things on scope passed down to react components - $scope.pluginDataStart = data; - $scope.storage = storage; - $scope.coreStart = coreStart; - $scope.loading = false; - $scope.reduxStore = store; - $scope.savedWorkspace = $route.current.locals.savedWorkspace; - - // register things for legacy angular UI - const allSavingDisabled = graphSavePolicy === 'none'; - $scope.spymode = 'request'; - $scope.colors = colorChoices; - $scope.isColorDark = (color) => isColorDark(...hexToRgb(color)); - $scope.nodeClick = function (n, $event) { - //Selection logic - shift key+click helps selects multiple nodes - // Without the shift key we deselect all prior selections (perhaps not - // a great idea for touch devices with no concept of shift key) - if (!$event.shiftKey) { - const prevSelection = n.isSelected; - $scope.workspace.selectNone(); - n.isSelected = prevSelection; - } - - if ($scope.workspace.toggleNodeSelection(n)) { - $scope.selectSelected(n); - } else { - $scope.detail = null; - } - }; - - $scope.clickEdge = function (edge) { - $scope.workspace.getAllIntersections($scope.handleMergeCandidatesCallback, [ - edge.topSrc, - edge.topTarget, - ]); - }; - - $scope.submit = function (searchTerm) { - $scope.workspaceInitialized = true; - const numHops = 2; - if (searchTerm.startsWith('{')) { - try { - const query = JSON.parse(searchTerm); - if (query.vertices) { - // Is a graph explore request - $scope.workspace.callElasticsearch(query); - } else { - // Is a regular query DSL query - $scope.workspace.search(query, $scope.liveResponseFields, numHops); - } - } catch (err) { - handleError(err); - } - return; - } - $scope.workspace.simpleSearch(searchTerm, $scope.liveResponseFields, numHops); - }; - - $scope.selectSelected = function (node) { - $scope.detail = { - latestNodeSelection: node, - }; - return ($scope.selectedSelectedVertex = node); - }; - - $scope.isSelectedSelected = function (node) { - return $scope.selectedSelectedVertex === node; - }; - - $scope.openUrlTemplate = function (template) { - const url = template.url; - const newUrl = url.replace(urlTemplateRegex, template.encoder.encode($scope.workspace)); - window.open(newUrl, '_blank'); - }; - - $scope.aceLoaded = (editor) => { - editor.$blockScrolling = Infinity; - }; - - $scope.setDetail = function (data) { - $scope.detail = data; - }; - - function canWipeWorkspace(callback, text, options) { - if (!hasFieldsSelector(store.getState())) { - callback(); - return; - } - const confirmModalOptions = { - confirmButtonText: i18n.translate('xpack.graph.leaveWorkspace.confirmButtonLabel', { - defaultMessage: 'Leave anyway', - }), - title: i18n.translate('xpack.graph.leaveWorkspace.modalTitle', { - defaultMessage: 'Unsaved changes', - }), - 'data-test-subj': 'confirmModal', - ...options, - }; - - overlays - .openConfirm( - text || - i18n.translate('xpack.graph.leaveWorkspace.confirmText', { - defaultMessage: 'If you leave now, you will lose unsaved changes.', - }), - confirmModalOptions - ) - .then((isConfirmed) => { - if (isConfirmed) { - callback(); - } - }); - } - $scope.confirmWipeWorkspace = canWipeWorkspace; - - $scope.performMerge = function (parentId, childId) { - let found = true; - while (found) { - found = false; - for (const i in $scope.detail.mergeCandidates) { - if ($scope.detail.mergeCandidates.hasOwnProperty(i)) { - const mc = $scope.detail.mergeCandidates[i]; - if (mc.id1 === childId || mc.id2 === childId) { - $scope.detail.mergeCandidates.splice(i, 1); - found = true; - break; - } - } - } - } - $scope.workspace.mergeIds(parentId, childId); - $scope.detail = null; - }; - - $scope.handleMergeCandidatesCallback = function (termIntersects) { - const mergeCandidates = []; - termIntersects.forEach((ti) => { - mergeCandidates.push({ - id1: ti.id1, - id2: ti.id2, - term1: ti.term1, - term2: ti.term2, - v1: ti.v1, - v2: ti.v2, - overlap: ti.overlap, - }); - }); - $scope.detail = { mergeCandidates }; - }; - - // ===== Menubar configuration ========= - $scope.setHeaderActionMenu = setHeaderActionMenu; - $scope.topNavMenu = []; - $scope.topNavMenu.push({ - key: 'new', - label: i18n.translate('xpack.graph.topNavMenu.newWorkspaceLabel', { - defaultMessage: 'New', - }), - description: i18n.translate('xpack.graph.topNavMenu.newWorkspaceAriaLabel', { - defaultMessage: 'New Workspace', - }), - tooltip: i18n.translate('xpack.graph.topNavMenu.newWorkspaceTooltip', { - defaultMessage: 'Create a new workspace', - }), - run: function () { - canWipeWorkspace(function () { - $scope.$evalAsync(() => { - if ($location.url() === '/workspace/') { - $route.reload(); - } else { - $location.url('/workspace/'); - } - }); - }); - }, - testId: 'graphNewButton', - }); - - // if saving is disabled using uiCapabilities, we don't want to render the save - // button so it's consistent with all of the other applications - if (capabilities.save) { - // allSavingDisabled is based on the xpack.graph.savePolicy, we'll maintain this functionality - - $scope.topNavMenu.push({ - key: 'save', - label: i18n.translate('xpack.graph.topNavMenu.saveWorkspace.enabledLabel', { - defaultMessage: 'Save', - }), - description: i18n.translate('xpack.graph.topNavMenu.saveWorkspace.enabledAriaLabel', { - defaultMessage: 'Save workspace', - }), - tooltip: () => { - if (allSavingDisabled) { - return i18n.translate('xpack.graph.topNavMenu.saveWorkspace.disabledTooltip', { - defaultMessage: - 'No changes to saved workspaces are permitted by the current save policy', - }); - } else { - return i18n.translate('xpack.graph.topNavMenu.saveWorkspace.enabledTooltip', { - defaultMessage: 'Save this workspace', - }); - } - }, - disableButton: function () { - return allSavingDisabled || !hasFieldsSelector(store.getState()); - }, - run: () => { - store.dispatch({ - type: 'x-pack/graph/SAVE_WORKSPACE', - payload: $route.current.locals.savedWorkspace, - }); - }, - testId: 'graphSaveButton', - }); - } - $scope.topNavMenu.push({ - key: 'inspect', - disableButton: function () { - return $scope.workspace === null; - }, - label: i18n.translate('xpack.graph.topNavMenu.inspectLabel', { - defaultMessage: 'Inspect', - }), - description: i18n.translate('xpack.graph.topNavMenu.inspectAriaLabel', { - defaultMessage: 'Inspect', - }), - run: () => { - $scope.$evalAsync(() => { - const curState = $scope.menus.showInspect; - $scope.closeMenus(); - $scope.menus.showInspect = !curState; - }); - }, - }); - - $scope.topNavMenu.push({ - key: 'settings', - disableButton: function () { - return datasourceSelector(store.getState()).type === 'none'; - }, - label: i18n.translate('xpack.graph.topNavMenu.settingsLabel', { - defaultMessage: 'Settings', - }), - description: i18n.translate('xpack.graph.topNavMenu.settingsAriaLabel', { - defaultMessage: 'Settings', - }), - run: () => { - const settingsObservable = asAngularSyncedObservable( - () => ({ - blocklistedNodes: $scope.workspace ? [...$scope.workspace.blocklistedNodes] : undefined, - unblocklistNode: $scope.workspace ? $scope.workspace.unblocklist : undefined, - canEditDrillDownUrls: canEditDrillDownUrls, - }), - $scope.$digest.bind($scope) - ); - coreStart.overlays.openFlyout( - toMountPoint( - - - - ), - { - size: 'm', - closeButtonAriaLabel: i18n.translate('xpack.graph.settings.closeLabel', { - defaultMessage: 'Close', - }), - 'data-test-subj': 'graphSettingsFlyout', - ownFocus: true, - className: 'gphSettingsFlyout', - maxWidth: 520, - } - ); - }, - }); - - // Allow URLs to include a user-defined text query - if ($route.current.params.query) { - $scope.initialQuery = $route.current.params.query; - const unbind = $scope.$watch('workspace', () => { - if (!$scope.workspace) { - return; - } - unbind(); - $scope.submit($route.current.params.query); - }); - } - - $scope.menus = { - showSettings: false, - }; - - $scope.closeMenus = () => { - _.forOwn($scope.menus, function (_, key) { - $scope.menus[key] = false; - }); - }; - - // Deal with situation of request to open saved workspace - if ($route.current.locals.savedWorkspace.id) { - store.dispatch({ - type: 'x-pack/graph/LOAD_WORKSPACE', - payload: $route.current.locals.savedWorkspace, - }); - } else { - $scope.noIndexPatterns = $route.current.locals.indexPatterns.length === 0; - } - }); - //End controller -} diff --git a/x-pack/plugins/graph/public/application.ts b/x-pack/plugins/graph/public/application.ts index 4d4b3c34de52b..7461a7b5fc172 100644 --- a/x-pack/plugins/graph/public/application.ts +++ b/x-pack/plugins/graph/public/application.ts @@ -5,20 +5,8 @@ * 2.0. */ -// inner angular imports -// these are necessary to bootstrap the local angular. -// They can stay even after NP cutover -import angular from 'angular'; -import { i18nDirective, i18nFilter, I18nProvider } from '@kbn/i18n/angular'; +import { i18n } from '@kbn/i18n'; -import 'brace'; -import 'brace/mode/json'; - -// required for i18nIdDirective and `ngSanitize` angular module -import 'angular-sanitize'; -// required for ngRoute -import 'angular-route'; -// type imports import { ChromeStart, CoreStart, @@ -28,23 +16,21 @@ import { OverlayStart, AppMountParameters, IUiSettingsClient, + Capabilities, + ScopedHistory, } from 'kibana/public'; -// @ts-ignore -import { initGraphApp } from './app'; +import ReactDOM from 'react-dom'; import { DataPlugin, IndexPatternsContract } from '../../../../src/plugins/data/public'; import { LicensingPluginStart } from '../../licensing/public'; import { checkLicense } from '../common/check_license'; import { NavigationPublicPluginStart as NavigationStart } from '../../../../src/plugins/navigation/public'; import { Storage } from '../../../../src/plugins/kibana_utils/public'; -import { - configureAppAngularModule, - createTopNavDirective, - createTopNavHelper, - KibanaLegacyStart, -} from '../../../../src/plugins/kibana_legacy/public'; +import { KibanaLegacyStart } from '../../../../src/plugins/kibana_legacy/public'; import './index.scss'; import { SavedObjectsStart } from '../../../../src/plugins/saved_objects/public'; +import { GraphSavePolicy } from './types'; +import { graphRouter } from './router'; /** * These are dependencies of the Graph app besides the base dependencies @@ -58,7 +44,7 @@ export interface GraphDependencies { coreStart: CoreStart; element: HTMLElement; appBasePath: string; - capabilities: Record>; + capabilities: Capabilities; navigation: NavigationStart; licensing: LicensingPluginStart; chrome: ChromeStart; @@ -70,22 +56,32 @@ export interface GraphDependencies { getBasePath: () => string; storage: Storage; canEditDrillDownUrls: boolean; - graphSavePolicy: string; + graphSavePolicy: GraphSavePolicy; overlays: OverlayStart; savedObjects: SavedObjectsStart; kibanaLegacy: KibanaLegacyStart; setHeaderActionMenu: AppMountParameters['setHeaderActionMenu']; uiSettings: IUiSettingsClient; + history: ScopedHistory; } -export const renderApp = ({ appBasePath, element, kibanaLegacy, ...deps }: GraphDependencies) => { +export type GraphServices = Omit; + +export const renderApp = ({ history, kibanaLegacy, element, ...deps }: GraphDependencies) => { + const { chrome, capabilities } = deps; kibanaLegacy.loadFontAwesome(); - const graphAngularModule = createLocalAngularModule(deps.navigation); - configureAppAngularModule( - graphAngularModule, - { core: deps.core, env: deps.pluginInitializerContext.env }, - true - ); + + if (!capabilities.graph.save) { + chrome.setBadge({ + text: i18n.translate('xpack.graph.badge.readOnly.text', { + defaultMessage: 'Read only', + }), + tooltip: i18n.translate('xpack.graph.badge.readOnly.tooltip', { + defaultMessage: 'Unable to save Graph workspaces', + }), + iconType: 'glasses', + }); + } const licenseSubscription = deps.licensing.license$.subscribe((license) => { const info = checkLicense(license); @@ -105,59 +101,19 @@ export const renderApp = ({ appBasePath, element, kibanaLegacy, ...deps }: Graph } }); - initGraphApp(graphAngularModule, deps); - const $injector = mountGraphApp(appBasePath, element); + // dispatch synthetic hash change event to update hash history objects + // this is necessary because hash updates triggered by using popState won't trigger this event naturally. + const unlistenParentHistory = history.listen(() => { + window.dispatchEvent(new HashChangeEvent('hashchange')); + }); + + const app = graphRouter(deps); + ReactDOM.render(app, element); + element.setAttribute('class', 'gphAppWrapper'); + return () => { licenseSubscription.unsubscribe(); - $injector.get('$rootScope').$destroy(); + unlistenParentHistory(); + ReactDOM.unmountComponentAtNode(element); }; }; - -const mainTemplate = (basePath: string) => `
- -
-`; - -const moduleName = 'app/graph'; - -const thirdPartyAngularDependencies = ['ngSanitize', 'ngRoute', 'react', 'ui.bootstrap']; - -function mountGraphApp(appBasePath: string, element: HTMLElement) { - const mountpoint = document.createElement('div'); - mountpoint.setAttribute('class', 'gphAppWrapper'); - // eslint-disable-next-line no-unsanitized/property - mountpoint.innerHTML = mainTemplate(appBasePath); - // bootstrap angular into detached element and attach it later to - // make angular-within-angular possible - const $injector = angular.bootstrap(mountpoint, [moduleName]); - element.appendChild(mountpoint); - element.setAttribute('class', 'gphAppWrapper'); - return $injector; -} - -function createLocalAngularModule(navigation: NavigationStart) { - createLocalI18nModule(); - createLocalTopNavModule(navigation); - - const graphAngularModule = angular.module(moduleName, [ - ...thirdPartyAngularDependencies, - 'graphI18n', - 'graphTopNav', - ]); - return graphAngularModule; -} - -function createLocalTopNavModule(navigation: NavigationStart) { - angular - .module('graphTopNav', ['react']) - .directive('kbnTopNav', createTopNavDirective) - .directive('kbnTopNavHelper', createTopNavHelper(navigation.ui)); -} - -function createLocalI18nModule() { - angular - .module('graphI18n', []) - .provider('i18n', I18nProvider) - .filter('i18n', i18nFilter) - .directive('i18nId', i18nDirective); -} diff --git a/x-pack/plugins/graph/public/components/listing.tsx b/x-pack/plugins/graph/public/apps/listing_route.tsx similarity index 64% rename from x-pack/plugins/graph/public/components/listing.tsx rename to x-pack/plugins/graph/public/apps/listing_route.tsx index 53fdab4a02885..e7457f18005e6 100644 --- a/x-pack/plugins/graph/public/components/listing.tsx +++ b/x-pack/plugins/graph/public/apps/listing_route.tsx @@ -5,30 +5,72 @@ * 2.0. */ +import React, { Fragment, useCallback, useEffect } from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage, I18nProvider } from '@kbn/i18n/react'; -import React, { Fragment } from 'react'; import { EuiEmptyPrompt, EuiLink, EuiButton } from '@elastic/eui'; - -import { CoreStart, ApplicationStart } from 'kibana/public'; +import { ApplicationStart } from 'kibana/public'; +import { useHistory, useLocation } from 'react-router-dom'; import { TableListView } from '../../../../../src/plugins/kibana_react/public'; +import { deleteSavedWorkspace, findSavedWorkspace } from '../helpers/saved_workspace_utils'; +import { getEditPath, getEditUrl, getNewPath, setBreadcrumbs } from '../services/url'; import { GraphWorkspaceSavedObject } from '../types'; +import { GraphServices } from '../application'; -export interface ListingProps { - coreStart: CoreStart; - createItem: () => void; - findItems: (query: string) => Promise<{ total: number; hits: GraphWorkspaceSavedObject[] }>; - deleteItems: (records: GraphWorkspaceSavedObject[]) => Promise; - editItem: (record: GraphWorkspaceSavedObject) => void; - getViewUrl: (record: GraphWorkspaceSavedObject) => string; - listingLimit: number; - hideWriteControls: boolean; - capabilities: { save: boolean; delete: boolean }; - initialFilter: string; - initialPageSize: number; +export interface ListingRouteProps { + deps: GraphServices; } -export function Listing(props: ListingProps) { +export function ListingRoute({ + deps: { chrome, savedObjects, savedObjectsClient, coreStart, capabilities, addBasePath }, +}: ListingRouteProps) { + const listingLimit = savedObjects.settings.getListingLimit(); + const initialPageSize = savedObjects.settings.getPerPage(); + const history = useHistory(); + const query = new URLSearchParams(useLocation().search); + const initialFilter = query.get('filter') || ''; + + useEffect(() => { + setBreadcrumbs({ chrome }); + }, [chrome]); + + const createItem = useCallback(() => { + history.push(getNewPath()); + }, [history]); + + const findItems = useCallback( + (search: string) => { + return findSavedWorkspace( + { savedObjectsClient, basePath: coreStart.http.basePath }, + search, + listingLimit + ); + }, + [coreStart.http.basePath, listingLimit, savedObjectsClient] + ); + + const editItem = useCallback( + (savedWorkspace: GraphWorkspaceSavedObject) => { + history.push(getEditPath(savedWorkspace)); + }, + [history] + ); + + const getViewUrl = useCallback( + (savedWorkspace: GraphWorkspaceSavedObject) => getEditUrl(addBasePath, savedWorkspace), + [addBasePath] + ); + + const deleteItems = useCallback( + async (savedWorkspaces: GraphWorkspaceSavedObject[]) => { + await deleteSavedWorkspace( + savedObjectsClient, + savedWorkspaces.map((cur) => cur.id!) + ); + }, + [savedObjectsClient] + ); + return ( { + /** + * It's temporary workaround, which should be removed after migration `workspace` to redux. + * Ref holds mutable `workspace` object. After each `workspace.methodName(...)` call + * (which might mutate `workspace` somehow), react state needs to be updated using + * `workspace.changeHandler()`. + */ + const workspaceRef = useRef(); + /** + * Providing `workspaceRef.current` to the hook dependencies or components itself + * will not leads to updates, therefore `renderCounter` is used to update react state. + */ + const [renderCounter, setRenderCounter] = useState(0); + const history = useHistory(); + const urlQuery = new URLSearchParams(useLocation().search).get('query'); + + const indexPatternProvider = useMemo( + () => createCachedIndexPatternProvider(getIndexPatternProvider.get), + [getIndexPatternProvider.get] + ); + + const { loading, callNodeProxy, callSearchNodeProxy, handleSearchQueryError } = useGraphLoader({ + toastNotifications, + coreStart, + }); + + const services = useMemo( + () => ({ + appName: 'graph', + storage, + data, + ...coreStart, + }), + [coreStart, data, storage] + ); + + const [store] = useState(() => + createGraphStore({ + basePath: getBasePath(), + addBasePath, + indexPatternProvider, + createWorkspace: (indexPattern, exploreControls) => { + const options = { + indexName: indexPattern, + vertex_fields: [], + // Here we have the opportunity to look up labels for nodes... + nodeLabeller() { + // console.log(newNodes); + }, + changeHandler: () => setRenderCounter((cur) => cur + 1), + graphExploreProxy: callNodeProxy, + searchProxy: callSearchNodeProxy, + exploreControls, + }; + const createdWorkspace = (workspaceRef.current = createWorkspace(options)); + return createdWorkspace; + }, + getWorkspace: () => workspaceRef.current, + notifications: coreStart.notifications, + http: coreStart.http, + overlays: coreStart.overlays, + savedObjectsClient, + showSaveModal, + savePolicy: graphSavePolicy, + changeUrl: (newUrl) => history.push(newUrl), + notifyReact: () => setRenderCounter((cur) => cur + 1), + chrome, + I18nContext: coreStart.i18n.Context, + handleSearchQueryError, + }) + ); + + const { savedWorkspace, indexPatterns } = useWorkspaceLoader({ + workspaceRef, + store, + savedObjectsClient, + toastNotifications, + }); + + if (!savedWorkspace || !indexPatterns) { + return null; + } + + return ( + + + + + + + + ); +}; diff --git a/x-pack/plugins/graph/public/badge.js b/x-pack/plugins/graph/public/badge.js deleted file mode 100644 index 128e30ee3f019..0000000000000 --- a/x-pack/plugins/graph/public/badge.js +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { i18n } from '@kbn/i18n'; - -export function getReadonlyBadge(uiCapabilities) { - if (uiCapabilities.graph.save) { - return null; - } - - return { - text: i18n.translate('xpack.graph.badge.readOnly.text', { - defaultMessage: 'Read only', - }), - tooltip: i18n.translate('xpack.graph.badge.readOnly.tooltip', { - defaultMessage: 'Unable to save Graph workspaces', - }), - iconType: 'glasses', - }; -} diff --git a/x-pack/plugins/graph/public/angular/templates/_graph.scss b/x-pack/plugins/graph/public/components/_graph.scss similarity index 75% rename from x-pack/plugins/graph/public/angular/templates/_graph.scss rename to x-pack/plugins/graph/public/components/_graph.scss index 5c2f5d5f7a881..706389304067c 100644 --- a/x-pack/plugins/graph/public/angular/templates/_graph.scss +++ b/x-pack/plugins/graph/public/components/_graph.scss @@ -1,11 +1,3 @@ -@mixin gphSvgText() { - font-family: $euiFontFamily; - font-size: $euiSizeS; - line-height: $euiSizeM; - fill: $euiColorDarkShade; - color: $euiColorDarkShade; -} - /** * THE SVG Graph * 1. Calculated px values come from the open/closed state of the global nav sidebar diff --git a/x-pack/plugins/graph/public/components/_index.scss b/x-pack/plugins/graph/public/components/_index.scss index a06209e7e4d34..743c24c896426 100644 --- a/x-pack/plugins/graph/public/components/_index.scss +++ b/x-pack/plugins/graph/public/components/_index.scss @@ -7,3 +7,6 @@ @import './settings/index'; @import './legacy_icon/index'; @import './field_manager/index'; +@import './graph'; +@import './sidebar'; +@import './inspect'; diff --git a/x-pack/plugins/graph/public/angular/templates/_inspect.scss b/x-pack/plugins/graph/public/components/_inspect.scss similarity index 100% rename from x-pack/plugins/graph/public/angular/templates/_inspect.scss rename to x-pack/plugins/graph/public/components/_inspect.scss diff --git a/x-pack/plugins/graph/public/angular/templates/_sidebar.scss b/x-pack/plugins/graph/public/components/_sidebar.scss similarity index 82% rename from x-pack/plugins/graph/public/angular/templates/_sidebar.scss rename to x-pack/plugins/graph/public/components/_sidebar.scss index e784649b250fa..831032231fe8c 100644 --- a/x-pack/plugins/graph/public/angular/templates/_sidebar.scss +++ b/x-pack/plugins/graph/public/components/_sidebar.scss @@ -24,6 +24,10 @@ padding: $euiSizeXS; border-radius: $euiBorderRadius; margin-bottom: $euiSizeXS; + + & > span { + padding-right: $euiSizeXS; + } } .gphSidebar__panel { @@ -35,8 +39,9 @@ * Vertex Select */ -.gphVertexSelect__button { - margin: $euiSizeXS $euiSizeXS $euiSizeXS 0; +.vertexSelectionTypesBar { + margin-top: 0; + margin-bottom: 0; } /** @@ -68,15 +73,24 @@ background: $euiColorLightShade; } +/** + * Link summary + */ + +.gphDrillDownIconLinks { + margin-top: .5 * $euiSizeXS; + margin-bottom: .5 * $euiSizeXS; +} + /** * Link summary */ .gphLinkSummary__term--1 { - color:$euiColorDanger; + color: $euiColorDanger; } .gphLinkSummary__term--2 { - color:$euiColorPrimary; + color: $euiColorPrimary; } .gphLinkSummary__term--1-2 { color: mix($euiColorDanger, $euiColorPrimary); diff --git a/x-pack/plugins/graph/public/components/app.tsx b/x-pack/plugins/graph/public/components/app.tsx deleted file mode 100644 index fbe7f2d3ebe86..0000000000000 --- a/x-pack/plugins/graph/public/components/app.tsx +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { EuiSpacer } from '@elastic/eui'; - -import { DataPublicPluginStart } from 'src/plugins/data/public'; -import { Provider } from 'react-redux'; -import React, { useState } from 'react'; -import { I18nProvider } from '@kbn/i18n/react'; -import { CoreStart } from 'kibana/public'; -import { IStorageWrapper } from 'src/plugins/kibana_utils/public'; -import { FieldManager } from './field_manager'; -import { SearchBarProps, SearchBar } from './search_bar'; -import { GraphStore } from '../state_management'; -import { GuidancePanel } from './guidance_panel'; -import { GraphTitle } from './graph_title'; - -import { KibanaContextProvider } from '../../../../../src/plugins/kibana_react/public'; - -export interface GraphAppProps extends SearchBarProps { - coreStart: CoreStart; - // This is not named dataStart because of Angular treating data- prefix differently - pluginDataStart: DataPublicPluginStart; - storage: IStorageWrapper; - reduxStore: GraphStore; - isInitialized: boolean; - noIndexPatterns: boolean; -} - -export function GraphApp(props: GraphAppProps) { - const [pickerOpen, setPickerOpen] = useState(false); - const { - coreStart, - pluginDataStart, - storage, - reduxStore, - noIndexPatterns, - ...searchBarProps - } = props; - - return ( - - - - <> - {props.isInitialized && } -
- - - -
- {!props.isInitialized && ( - { - setPickerOpen(true); - }} - /> - )} - -
-
-
- ); -} diff --git a/x-pack/plugins/graph/public/components/control_panel/control_panel.tsx b/x-pack/plugins/graph/public/components/control_panel/control_panel.tsx new file mode 100644 index 0000000000000..2946bc8ad56f5 --- /dev/null +++ b/x-pack/plugins/graph/public/components/control_panel/control_panel.tsx @@ -0,0 +1,143 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { connect } from 'react-redux'; +import { + ControlType, + TermIntersect, + UrlTemplate, + Workspace, + WorkspaceField, + WorkspaceNode, +} from '../../types'; +import { urlTemplateRegex } from '../../helpers/url_template'; +import { SelectionToolBar } from './selection_tool_bar'; +import { ControlPanelToolBar } from './control_panel_tool_bar'; +import { SelectStyle } from './select_style'; +import { SelectedNodeEditor } from './selected_node_editor'; +import { MergeCandidates } from './merge_candidates'; +import { DrillDowns } from './drill_downs'; +import { DrillDownIconLinks } from './drill_down_icon_links'; +import { GraphState, liveResponseFieldsSelector, templatesSelector } from '../../state_management'; +import { SelectedNodeItem } from './selected_node_item'; + +export interface TargetOptions { + toFields: WorkspaceField[]; +} + +interface ControlPanelProps { + renderCounter: number; + workspace: Workspace; + control: ControlType; + selectedNode?: WorkspaceNode; + colors: string[]; + mergeCandidates: TermIntersect[]; + onSetControl: (control: ControlType) => void; + selectSelected: (node: WorkspaceNode) => void; +} + +interface ControlPanelStateProps { + urlTemplates: UrlTemplate[]; + liveResponseFields: WorkspaceField[]; +} + +const ControlPanelComponent = ({ + workspace, + liveResponseFields, + urlTemplates, + control, + selectedNode, + colors, + mergeCandidates, + onSetControl, + selectSelected, +}: ControlPanelProps & ControlPanelStateProps) => { + const hasNodes = workspace.nodes.length === 0; + + const openUrlTemplate = (template: UrlTemplate) => { + const url = template.url; + const newUrl = url.replace(urlTemplateRegex, template.encoder.encode(workspace!)); + window.open(newUrl, '_blank'); + }; + + const onSelectedFieldClick = (node: WorkspaceNode) => { + selectSelected(node); + workspace.changeHandler(); + }; + + const onDeselectNode = (node: WorkspaceNode) => { + workspace.deselectNode(node); + workspace.changeHandler(); + onSetControl('none'); + }; + + return ( + + ); +}; + +export const ControlPanel = connect((state: GraphState) => ({ + urlTemplates: templatesSelector(state), + liveResponseFields: liveResponseFieldsSelector(state), +}))(ControlPanelComponent); diff --git a/x-pack/plugins/graph/public/components/control_panel/control_panel_tool_bar.tsx b/x-pack/plugins/graph/public/components/control_panel/control_panel_tool_bar.tsx new file mode 100644 index 0000000000000..37a9c003f7682 --- /dev/null +++ b/x-pack/plugins/graph/public/components/control_panel/control_panel_tool_bar.tsx @@ -0,0 +1,230 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiFlexGroup, EuiFlexItem, EuiToolTip } from '@elastic/eui'; +import { ControlType, Workspace, WorkspaceField } from '../../types'; + +interface ControlPanelToolBarProps { + workspace: Workspace; + liveResponseFields: WorkspaceField[]; + onSetControl: (action: ControlType) => void; +} + +export const ControlPanelToolBar = ({ + workspace, + onSetControl, + liveResponseFields, +}: ControlPanelToolBarProps) => { + const haveNodes = workspace.nodes.length === 0; + + const undoButtonMsg = i18n.translate('xpack.graph.sidebar.topMenu.undoButtonTooltip', { + defaultMessage: 'Undo', + }); + const redoButtonMsg = i18n.translate('xpack.graph.sidebar.topMenu.redoButtonTooltip', { + defaultMessage: 'Redo', + }); + const expandButtonMsg = i18n.translate( + 'xpack.graph.sidebar.topMenu.expandSelectionButtonTooltip', + { + defaultMessage: 'Expand selection', + } + ); + const addLinksButtonMsg = i18n.translate('xpack.graph.sidebar.topMenu.addLinksButtonTooltip', { + defaultMessage: 'Add links between existing terms', + }); + const removeVerticesButtonMsg = i18n.translate( + 'xpack.graph.sidebar.topMenu.removeVerticesButtonTooltip', + { + defaultMessage: 'Remove vertices from workspace', + } + ); + const blocklistButtonMsg = i18n.translate('xpack.graph.sidebar.topMenu.blocklistButtonTooltip', { + defaultMessage: 'Block selection from appearing in workspace', + }); + const customStyleButtonMsg = i18n.translate( + 'xpack.graph.sidebar.topMenu.customStyleButtonTooltip', + { + defaultMessage: 'Custom style selected vertices', + } + ); + const drillDownButtonMsg = i18n.translate('xpack.graph.sidebar.topMenu.drillDownButtonTooltip', { + defaultMessage: 'Drill down', + }); + const runLayoutButtonMsg = i18n.translate('xpack.graph.sidebar.topMenu.runLayoutButtonTooltip', { + defaultMessage: 'Run layout', + }); + const pauseLayoutButtonMsg = i18n.translate( + 'xpack.graph.sidebar.topMenu.pauseLayoutButtonTooltip', + { + defaultMessage: 'Pause layout', + } + ); + + const onUndoClick = () => workspace.undo(); + const onRedoClick = () => workspace.redo(); + const onExpandButtonClick = () => { + onSetControl('none'); + workspace.expandSelecteds({ toFields: liveResponseFields }); + }; + const onAddLinksClick = () => workspace.fillInGraph(); + const onRemoveVerticesClick = () => { + onSetControl('none'); + workspace.deleteSelection(); + }; + const onBlockListClick = () => workspace.blocklistSelection(); + const onCustomStyleClick = () => onSetControl('style'); + const onDrillDownClick = () => onSetControl('drillDowns'); + const onRunLayoutClick = () => workspace.runLayout(); + const onPauseLayoutClick = () => { + workspace.stopLayout(); + workspace.changeHandler(); + }; + + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {(workspace.nodes.length === 0 || workspace.force === null) && ( + + + + + + )} + + {workspace.force !== null && workspace.nodes.length > 0 && ( + + + + + + )} + + ); +}; diff --git a/x-pack/plugins/graph/public/components/control_panel/drill_down_icon_links.tsx b/x-pack/plugins/graph/public/components/control_panel/drill_down_icon_links.tsx new file mode 100644 index 0000000000000..8d92d6ca04007 --- /dev/null +++ b/x-pack/plugins/graph/public/components/control_panel/drill_down_icon_links.tsx @@ -0,0 +1,61 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiFlexGroup, EuiFlexItem, EuiToolTip } from '@elastic/eui'; +import React from 'react'; +import { UrlTemplate } from '../../types'; + +interface UrlTemplateButtonsProps { + urlTemplates: UrlTemplate[]; + hasNodes: boolean; + openUrlTemplate: (template: UrlTemplate) => void; +} + +export const DrillDownIconLinks = ({ + hasNodes, + urlTemplates, + openUrlTemplate, +}: UrlTemplateButtonsProps) => { + const drillDownsWithIcons = urlTemplates.filter( + ({ icon }: UrlTemplate) => icon && icon.class !== '' + ); + + if (drillDownsWithIcons.length === 0) { + return null; + } + + const drillDowns = drillDownsWithIcons.map((cur) => { + const onUrlTemplateClick = () => openUrlTemplate(cur); + + return ( + + + + + + ); + }); + + return ( + + {drillDowns} + + ); +}; diff --git a/x-pack/plugins/graph/public/components/control_panel/drill_downs.tsx b/x-pack/plugins/graph/public/components/control_panel/drill_downs.tsx new file mode 100644 index 0000000000000..9d0dfdc7ba705 --- /dev/null +++ b/x-pack/plugins/graph/public/components/control_panel/drill_downs.tsx @@ -0,0 +1,55 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { UrlTemplate } from '../../types'; + +interface DrillDownsProps { + urlTemplates: UrlTemplate[]; + openUrlTemplate: (template: UrlTemplate) => void; +} + +export const DrillDowns = ({ urlTemplates, openUrlTemplate }: DrillDownsProps) => { + return ( +
+
+ + {i18n.translate('xpack.graph.sidebar.drillDownsTitle', { + defaultMessage: 'Drill-downs', + })} +
+ +
+ {urlTemplates.length === 0 && ( +

+ {i18n.translate('xpack.graph.sidebar.drillDowns.noDrillDownsHelpText', { + defaultMessage: 'Configure drill-downs from the settings menu', + })} +

+ )} + +
    + {urlTemplates.map((urlTemplate) => { + const onOpenUrlTemplate = () => openUrlTemplate(urlTemplate); + + return ( +
  • + {urlTemplate.icon && ( + {urlTemplate.icon?.code} + )} + +
  • + ); + })} +
+
+
+ ); +}; diff --git a/x-pack/plugins/graph/public/components/control_panel/index.ts b/x-pack/plugins/graph/public/components/control_panel/index.ts new file mode 100644 index 0000000000000..7c3ab15baea2d --- /dev/null +++ b/x-pack/plugins/graph/public/components/control_panel/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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export * from './control_panel'; diff --git a/x-pack/plugins/graph/public/components/control_panel/merge_candidates.tsx b/x-pack/plugins/graph/public/components/control_panel/merge_candidates.tsx new file mode 100644 index 0000000000000..cc380993ef996 --- /dev/null +++ b/x-pack/plugins/graph/public/components/control_panel/merge_candidates.tsx @@ -0,0 +1,137 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiToolTip } from '@elastic/eui'; +import { ControlType, TermIntersect, Workspace } from '../../types'; +import { VennDiagram } from '../venn_diagram'; + +interface MergeCandidatesProps { + workspace: Workspace; + mergeCandidates: TermIntersect[]; + onSetControl: (control: ControlType) => void; +} + +export const MergeCandidates = ({ + workspace, + mergeCandidates, + onSetControl, +}: MergeCandidatesProps) => { + const performMerge = (parentId: string, childId: string) => { + const tempMergeCandidates = [...mergeCandidates]; + let found = true; + while (found) { + found = false; + + for (let i = 0; i < tempMergeCandidates.length; i++) { + const term = tempMergeCandidates[i]; + if (term.id1 === childId || term.id2 === childId) { + tempMergeCandidates.splice(i, 1); + found = true; + break; + } + } + } + workspace.mergeIds(parentId, childId); + onSetControl('none'); + }; + + return ( +
+
+ + {i18n.translate('xpack.graph.sidebar.linkSummaryTitle', { + defaultMessage: 'Link summary', + })} +
+ {mergeCandidates.map((mc) => { + const mergeTerm1ToTerm2ButtonMsg = i18n.translate( + 'xpack.graph.sidebar.linkSummary.mergeTerm1ToTerm2ButtonTooltip', + { + defaultMessage: 'Merge {term1} into {term2}', + values: { term1: mc.term1, term2: mc.term2 }, + } + ); + const mergeTerm2ToTerm1ButtonMsg = i18n.translate( + 'xpack.graph.sidebar.linkSummary.mergeTerm2ToTerm1ButtonTooltip', + { + defaultMessage: 'Merge {term2} into {term1}', + values: { term1: mc.term1, term2: mc.term2 }, + } + ); + const leftTermCountMsg = i18n.translate( + 'xpack.graph.sidebar.linkSummary.leftTermCountTooltip', + { + defaultMessage: '{count} documents have term {term}', + values: { count: mc.v1, term: mc.term1 }, + } + ); + const bothTermsCountMsg = i18n.translate( + 'xpack.graph.sidebar.linkSummary.bothTermsCountTooltip', + { + defaultMessage: '{count} documents have both terms', + values: { count: mc.overlap }, + } + ); + const rightTermCountMsg = i18n.translate( + 'xpack.graph.sidebar.linkSummary.rightTermCountTooltip', + { + defaultMessage: '{count} documents have term {term}', + values: { count: mc.v2, term: mc.term2 }, + } + ); + + const onMergeTerm1ToTerm2Click = () => performMerge(mc.id2, mc.id1); + const onMergeTerm2ToTerm1Click = () => performMerge(mc.id1, mc.id2); + + return ( +
+ + + + + + {mc.term1} + {mc.term2} + + + + + + + + + + {mc.v1} + + +  ({mc.overlap})  + + + {mc.v2} + +
+ ); + })} +
+ ); +}; diff --git a/x-pack/plugins/graph/public/components/control_panel/select_style.tsx b/x-pack/plugins/graph/public/components/control_panel/select_style.tsx new file mode 100644 index 0000000000000..2dbefc7d24459 --- /dev/null +++ b/x-pack/plugins/graph/public/components/control_panel/select_style.tsx @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { Workspace } from '../../types'; + +interface SelectStyleProps { + workspace: Workspace; + colors: string[]; +} + +export const SelectStyle = ({ colors, workspace }: SelectStyleProps) => { + return ( +
+
+ + {i18n.translate('xpack.graph.sidebar.styleVerticesTitle', { + defaultMessage: 'Style selected vertices', + })} +
+ +
+ {colors.map((c) => { + const onSelectColor = () => { + workspace.colorSelected(c); + workspace.changeHandler(); + }; + return ( +
+
+ ); +}; diff --git a/x-pack/plugins/graph/public/components/control_panel/selected_node_editor.tsx b/x-pack/plugins/graph/public/components/control_panel/selected_node_editor.tsx new file mode 100644 index 0000000000000..a0eed56fac672 --- /dev/null +++ b/x-pack/plugins/graph/public/components/control_panel/selected_node_editor.tsx @@ -0,0 +1,100 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiToolTip } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { Workspace, WorkspaceNode } from '../../types'; + +interface SelectedNodeEditorProps { + workspace: Workspace; + selectedNode: WorkspaceNode; +} + +export const SelectedNodeEditor = ({ workspace, selectedNode }: SelectedNodeEditorProps) => { + const groupButtonMsg = i18n.translate('xpack.graph.sidebar.groupButtonTooltip', { + defaultMessage: 'group the currently selected items into {latestSelectionLabel}', + values: { latestSelectionLabel: selectedNode.label }, + }); + const ungroupButtonMsg = i18n.translate('xpack.graph.sidebar.ungroupButtonTooltip', { + defaultMessage: 'ungroup {latestSelectionLabel}', + values: { latestSelectionLabel: selectedNode.label }, + }); + + const onGroupButtonClick = () => { + workspace.groupSelections(selectedNode); + }; + const onClickUngroup = () => { + workspace.ungroup(selectedNode); + }; + const onChangeSelectedVertexLabel = (event: React.ChangeEvent) => { + selectedNode.label = event.target.value; + workspace.changeHandler(); + }; + + return ( +
+
+ {selectedNode.icon && } + {selectedNode.data.field} {selectedNode.data.term} +
+ + {(workspace.selectedNodes.length > 1 || + (workspace.selectedNodes.length > 0 && workspace.selectedNodes[0] !== selectedNode)) && ( + + + + )} + + {selectedNode.numChildren > 0 && ( + + + + )} + +
+
+ +
+ element && (element.value = selectedNode.label)} + type="text" + id="labelEdit" + className="form-control input-sm" + onChange={onChangeSelectedVertexLabel} + /> +
+ {i18n.translate('xpack.graph.sidebar.displayLabelHelpText', { + defaultMessage: 'Change the label for this vertex.', + })} +
+
+
+
+
+ ); +}; diff --git a/x-pack/plugins/graph/public/components/control_panel/selected_node_item.tsx b/x-pack/plugins/graph/public/components/control_panel/selected_node_item.tsx new file mode 100644 index 0000000000000..11df3b5d52086 --- /dev/null +++ b/x-pack/plugins/graph/public/components/control_panel/selected_node_item.tsx @@ -0,0 +1,63 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { hexToRgb, isColorDark } from '@elastic/eui'; +import classNames from 'classnames'; +import React from 'react'; +import { WorkspaceNode } from '../../types'; + +const isHexColorDark = (color: string) => isColorDark(...hexToRgb(color)); + +interface SelectedNodeItemProps { + node: WorkspaceNode; + isHighlighted: boolean; + onDeselectNode: (node: WorkspaceNode) => void; + onSelectedFieldClick: (node: WorkspaceNode) => void; +} + +export const SelectedNodeItem = ({ + node, + isHighlighted, + onSelectedFieldClick, + onDeselectNode, +}: SelectedNodeItemProps) => { + const fieldClasses = classNames('gphSelectionList__field', { + ['gphSelectionList__field--selected']: isHighlighted, + }); + const fieldIconClasses = classNames('fa', 'gphNode__text', 'gphSelectionList__icon', { + ['gphNode__text--inverse']: isHexColorDark(node.color), + }); + + return ( + + ); +}; diff --git a/x-pack/plugins/graph/public/components/control_panel/selection_tool_bar.tsx b/x-pack/plugins/graph/public/components/control_panel/selection_tool_bar.tsx new file mode 100644 index 0000000000000..e2e9771a8e9ef --- /dev/null +++ b/x-pack/plugins/graph/public/components/control_panel/selection_tool_bar.tsx @@ -0,0 +1,136 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiFlexGroup, EuiFlexItem, EuiToolTip } from '@elastic/eui'; +import { ControlType, Workspace } from '../../types'; + +interface SelectionToolBarProps { + workspace: Workspace; + onSetControl: (data: ControlType) => void; +} + +export const SelectionToolBar = ({ workspace, onSetControl }: SelectionToolBarProps) => { + const haveNodes = workspace.nodes.length === 0; + + const selectAllButtonMsg = i18n.translate( + 'xpack.graph.sidebar.selections.selectAllButtonTooltip', + { + defaultMessage: 'Select all', + } + ); + const selectNoneButtonMsg = i18n.translate( + 'xpack.graph.sidebar.selections.selectNoneButtonTooltip', + { + defaultMessage: 'Select none', + } + ); + const invertSelectionButtonMsg = i18n.translate( + 'xpack.graph.sidebar.selections.invertSelectionButtonTooltip', + { + defaultMessage: 'Invert selection', + } + ); + const selectNeighboursButtonMsg = i18n.translate( + 'xpack.graph.sidebar.selections.selectNeighboursButtonTooltip', + { + defaultMessage: 'Select neighbours', + } + ); + + const onSelectAllClick = () => { + onSetControl('none'); + workspace.selectAll(); + workspace.changeHandler(); + }; + const onSelectNoneClick = () => { + onSetControl('none'); + workspace.selectNone(); + workspace.changeHandler(); + }; + const onInvertSelectionClick = () => { + onSetControl('none'); + workspace.selectInvert(); + workspace.changeHandler(); + }; + const onSelectNeighboursClick = () => { + onSetControl('none'); + workspace.selectNeighbours(); + workspace.changeHandler(); + }; + + return ( + + + + + + + + + + + + + + + + + + + + + + + ); +}; diff --git a/x-pack/plugins/graph/public/components/graph_visualization/_graph_visualization.scss b/x-pack/plugins/graph/public/components/graph_visualization/_graph_visualization.scss index caef2b6987ddd..0853ab4114595 100644 --- a/x-pack/plugins/graph/public/components/graph_visualization/_graph_visualization.scss +++ b/x-pack/plugins/graph/public/components/graph_visualization/_graph_visualization.scss @@ -1,3 +1,11 @@ +@mixin gphSvgText() { + font-family: $euiFontFamily; + font-size: $euiSizeS; + line-height: $euiSizeM; + fill: $euiColorDarkShade; + color: $euiColorDarkShade; +} + .gphVisualization { flex: 1; display: flex; diff --git a/x-pack/plugins/graph/public/components/graph_visualization/graph_visualization.test.tsx b/x-pack/plugins/graph/public/components/graph_visualization/graph_visualization.test.tsx index f49b5bfd32da8..1ae556a79edcb 100644 --- a/x-pack/plugins/graph/public/components/graph_visualization/graph_visualization.test.tsx +++ b/x-pack/plugins/graph/public/components/graph_visualization/graph_visualization.test.tsx @@ -7,15 +7,13 @@ import React from 'react'; import { shallow } from 'enzyme'; -import { - GraphVisualization, - GroupAwareWorkspaceNode, - GroupAwareWorkspaceEdge, -} from './graph_visualization'; +import { GraphVisualization } from './graph_visualization'; +import { Workspace, WorkspaceEdge, WorkspaceNode } from '../../types'; describe('graph_visualization', () => { - const nodes: GroupAwareWorkspaceNode[] = [ + const nodes: WorkspaceNode[] = [ { + id: '1', color: 'black', data: { field: 'A', @@ -37,6 +35,7 @@ describe('graph_visualization', () => { y: 5, }, { + id: '2', color: 'red', data: { field: 'B', @@ -58,6 +57,7 @@ describe('graph_visualization', () => { y: 9, }, { + id: '3', color: 'yellow', data: { field: 'C', @@ -79,7 +79,7 @@ describe('graph_visualization', () => { y: 9, }, ]; - const edges: GroupAwareWorkspaceEdge[] = [ + const edges: WorkspaceEdge[] = [ { isSelected: true, label: '', @@ -101,9 +101,32 @@ describe('graph_visualization', () => { width: 2.2, }, ]; + const workspace = ({ + nodes, + edges, + selectNone: () => {}, + changeHandler: jest.fn(), + toggleNodeSelection: jest.fn().mockImplementation((node: WorkspaceNode) => { + return !node.isSelected; + }), + getAllIntersections: jest.fn(), + } as unknown) as jest.Mocked; + + beforeEach(() => { + jest.clearAllMocks(); + }); + it('should render empty workspace without data', () => { - expect(shallow( {}} nodeClick={() => {}} />)) - .toMatchInlineSnapshot(` + expect( + shallow( + {}} + onSetControl={() => {}} + onSetMergeCandidates={() => {}} + /> + ) + ).toMatchInlineSnapshot(` { it('should render to svg elements', () => { expect( shallow( - {}} nodeClick={() => {}} nodes={nodes} edges={edges} /> + {}} + onSetControl={() => {}} + onSetMergeCandidates={() => {}} + /> ) ).toMatchSnapshot(); }); - it('should react to node click', () => { - const nodeClickSpy = jest.fn(); + it('should react to node selection', () => { + const selectSelectedMock = jest.fn(); + const instance = shallow( {}} - nodeClick={nodeClickSpy} - nodes={nodes} - edges={edges} + workspace={workspace} + selectSelected={selectSelectedMock} + onSetControl={() => {}} + onSetMergeCandidates={() => {}} /> ); + + instance.find('.gphNode').last().simulate('click', {}); + + expect(workspace.toggleNodeSelection).toHaveBeenCalledWith(nodes[2]); + expect(selectSelectedMock).toHaveBeenCalledWith(nodes[2]); + expect(workspace.changeHandler).toHaveBeenCalled(); + }); + + it('should react to node deselection', () => { + const onSetControlMock = jest.fn(); + const instance = shallow( + {}} + onSetControl={onSetControlMock} + onSetMergeCandidates={() => {}} + /> + ); + instance.find('.gphNode').first().simulate('click', {}); - expect(nodeClickSpy).toHaveBeenCalledWith(nodes[0], {}); + + expect(workspace.toggleNodeSelection).toHaveBeenCalledWith(nodes[0]); + expect(onSetControlMock).toHaveBeenCalledWith('none'); + expect(workspace.changeHandler).toHaveBeenCalled(); }); it('should react to edge click', () => { - const edgeClickSpy = jest.fn(); const instance = shallow( {}} - nodes={nodes} - edges={edges} + workspace={workspace} + selectSelected={() => {}} + onSetControl={() => {}} + onSetMergeCandidates={() => {}} /> ); + instance.find('.gphEdge').first().simulate('click'); - expect(edgeClickSpy).toHaveBeenCalledWith(edges[0]); + + expect(workspace.getAllIntersections).toHaveBeenCalled(); + expect(edges[0].topSrc).toEqual(workspace.getAllIntersections.mock.calls[0][1][0]); + expect(edges[0].topTarget).toEqual(workspace.getAllIntersections.mock.calls[0][1][1]); }); }); diff --git a/x-pack/plugins/graph/public/components/graph_visualization/graph_visualization.tsx b/x-pack/plugins/graph/public/components/graph_visualization/graph_visualization.tsx index 9b8dc98b84f47..26359101a9a5b 100644 --- a/x-pack/plugins/graph/public/components/graph_visualization/graph_visualization.tsx +++ b/x-pack/plugins/graph/public/components/graph_visualization/graph_visualization.tsx @@ -9,31 +9,14 @@ import React, { useRef } from 'react'; import classNames from 'classnames'; import d3, { ZoomEvent } from 'd3'; import { isColorDark, hexToRgb } from '@elastic/eui'; -import { WorkspaceNode, WorkspaceEdge } from '../../types'; +import { Workspace, WorkspaceNode, TermIntersect, ControlType, WorkspaceEdge } from '../../types'; import { makeNodeId } from '../../services/persistence'; -/* - * The layouting algorithm sets a few extra properties on - * node objects to handle grouping. This will be moved to - * a separate data structure when the layouting is migrated - */ - -export interface GroupAwareWorkspaceNode extends WorkspaceNode { - kx: number; - ky: number; - numChildren: number; -} - -export interface GroupAwareWorkspaceEdge extends WorkspaceEdge { - topTarget: GroupAwareWorkspaceNode; - topSrc: GroupAwareWorkspaceNode; -} - export interface GraphVisualizationProps { - nodes?: GroupAwareWorkspaceNode[]; - edges?: GroupAwareWorkspaceEdge[]; - edgeClick: (edge: GroupAwareWorkspaceEdge) => void; - nodeClick: (node: GroupAwareWorkspaceNode, e: React.MouseEvent) => void; + workspace: Workspace; + onSetControl: (control: ControlType) => void; + selectSelected: (node: WorkspaceNode) => void; + onSetMergeCandidates: (terms: TermIntersect[]) => void; } function registerZooming(element: SVGSVGElement) { @@ -55,13 +38,39 @@ function registerZooming(element: SVGSVGElement) { } export function GraphVisualization({ - nodes, - edges, - edgeClick, - nodeClick, + workspace, + selectSelected, + onSetControl, + onSetMergeCandidates, }: GraphVisualizationProps) { const svgRoot = useRef(null); + const nodeClick = (n: WorkspaceNode, event: React.MouseEvent) => { + // Selection logic - shift key+click helps selects multiple nodes + // Without the shift key we deselect all prior selections (perhaps not + // a great idea for touch devices with no concept of shift key) + if (!event.shiftKey) { + const prevSelection = n.isSelected; + workspace.selectNone(); + n.isSelected = prevSelection; + } + if (workspace.toggleNodeSelection(n)) { + selectSelected(n); + } else { + onSetControl('none'); + } + workspace.changeHandler(); + }; + + const handleMergeCandidatesCallback = (termIntersects: TermIntersect[]) => { + const mergeCandidates: TermIntersect[] = [...termIntersects]; + onSetMergeCandidates(mergeCandidates); + onSetControl('mergeTerms'); + }; + + const edgeClick = (edge: WorkspaceEdge) => + workspace.getAllIntersections(handleMergeCandidatesCallback, [edge.topSrc, edge.topTarget]); + return ( - {edges && - edges.map((edge) => ( + {workspace.edges && + workspace.edges.map((edge) => ( ))} - {nodes && - nodes + {workspace.nodes && + workspace.nodes .filter((node) => !node.parent) .map((node) => ( {}; + +export const InspectPanel = ({ + showInspect, + lastRequest, + lastResponse, + indexPattern, +}: InspectPanelProps) => { + const [selectedTabId, setSelectedTabId] = useState('request'); + + const onRequestClick = () => setSelectedTabId('request'); + const onResponseClick = () => setSelectedTabId('response'); + + const editorContent = useMemo(() => (selectedTabId === 'request' ? lastRequest : lastResponse), [ + lastRequest, + lastResponse, + selectedTabId, + ]); + + if (showInspect) { + return ( +
+
+
+ +
+ +
+ + http://host:port/{indexPattern?.id}/_graph/explore + + + + + + + + + + +
+
+
+ ); + } + + return null; +}; diff --git a/x-pack/plugins/graph/public/components/inspect_panel/inspect_panel.tsx b/x-pack/plugins/graph/public/components/inspect_panel/inspect_panel.tsx deleted file mode 100644 index 2f29849bebcec..0000000000000 --- a/x-pack/plugins/graph/public/components/inspect_panel/inspect_panel.tsx +++ /dev/null @@ -1,109 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React, { useMemo, useState } from 'react'; -import { EuiTab, EuiTabs, EuiText } from '@elastic/eui'; -import { monaco, XJsonLang } from '@kbn/monaco'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { IUiSettingsClient } from 'kibana/public'; -import { IndexPattern } from '../../../../../../src/plugins/data/public'; -import { - CodeEditor, - KibanaContextProvider, -} from '../../../../../../src/plugins/kibana_react/public'; - -interface InspectPanelProps { - showInspect?: boolean; - indexPattern?: IndexPattern; - uiSettings: IUiSettingsClient; - lastRequest?: string; - lastResponse?: string; -} - -const CODE_EDITOR_OPTIONS: monaco.editor.IStandaloneEditorConstructionOptions = { - automaticLayout: true, - fontSize: 12, - lineNumbers: 'on', - minimap: { - enabled: false, - }, - overviewRulerBorder: false, - readOnly: true, - scrollbar: { - alwaysConsumeMouseWheel: false, - }, - scrollBeyondLastLine: false, - wordWrap: 'on', - wrappingIndent: 'indent', -}; - -const dummyCallback = () => {}; - -export const InspectPanel = ({ - showInspect, - lastRequest, - lastResponse, - indexPattern, - uiSettings, -}: InspectPanelProps) => { - const [selectedTabId, setSelectedTabId] = useState('request'); - - const onRequestClick = () => setSelectedTabId('request'); - const onResponseClick = () => setSelectedTabId('response'); - - const services = useMemo(() => ({ uiSettings }), [uiSettings]); - - const editorContent = useMemo(() => (selectedTabId === 'request' ? lastRequest : lastResponse), [ - selectedTabId, - lastRequest, - lastResponse, - ]); - - if (showInspect) { - return ( - -
-
-
- -
- -
- - http://host:port/{indexPattern?.id}/_graph/explore - - - - - - - - - - -
-
-
-
- ); - } - - return null; -}; diff --git a/x-pack/plugins/graph/public/components/search_bar.test.tsx b/x-pack/plugins/graph/public/components/search_bar.test.tsx index 690fdf832c373..1b76cde1a62fb 100644 --- a/x-pack/plugins/graph/public/components/search_bar.test.tsx +++ b/x-pack/plugins/graph/public/components/search_bar.test.tsx @@ -6,18 +6,18 @@ */ import { mountWithIntl } from '@kbn/test/jest'; -import { SearchBar, OuterSearchBarProps } from './search_bar'; -import React, { ReactElement } from 'react'; +import { SearchBar, SearchBarProps } from './search_bar'; +import React, { Component, ReactElement } from 'react'; import { CoreStart } from 'src/core/public'; import { act } from 'react-dom/test-utils'; import { IndexPattern, QueryStringInput } from '../../../../../src/plugins/data/public'; import { KibanaContextProvider } from '../../../../../src/plugins/kibana_react/public'; -import { I18nProvider } from '@kbn/i18n/react'; +import { I18nProvider, InjectedIntl } from '@kbn/i18n/react'; import { openSourceModal } from '../services/source_modal'; -import { GraphStore, setDatasource } from '../state_management'; +import { GraphStore, setDatasource, submitSearchSaga } from '../state_management'; import { ReactWrapper } from 'enzyme'; import { createMockGraphStore } from '../state_management/mocks'; import { Provider } from 'react-redux'; @@ -26,7 +26,7 @@ jest.mock('../services/source_modal', () => ({ openSourceModal: jest.fn() })); const waitForIndexPatternFetch = () => new Promise((r) => setTimeout(r)); -function wrapSearchBarInContext(testProps: OuterSearchBarProps) { +function wrapSearchBarInContext(testProps: SearchBarProps) { const services = { uiSettings: { get: (key: string) => { @@ -67,21 +67,34 @@ function wrapSearchBarInContext(testProps: OuterSearchBarProps) { } describe('search_bar', () => { + let dispatchSpy: jest.Mock; + let instance: ReactWrapper< + SearchBarProps & { intl: InjectedIntl }, + Readonly<{}>, + Component<{}, {}, any> + >; + let store: GraphStore; const defaultProps = { isLoading: false, - onQuerySubmit: jest.fn(), indexPatternProvider: { get: jest.fn(() => Promise.resolve(({ fields: [] } as unknown) as IndexPattern)), }, confirmWipeWorkspace: (callback: () => void) => { callback(); }, + onIndexPatternChange: (indexPattern?: IndexPattern) => { + instance.setProps({ + ...defaultProps, + currentIndexPattern: indexPattern, + }); + }, }; - let instance: ReactWrapper; - let store: GraphStore; beforeEach(() => { - store = createMockGraphStore({}).store; + store = createMockGraphStore({ + sagas: [submitSearchSaga], + }).store; + store.dispatch( setDatasource({ type: 'indexpattern', @@ -89,14 +102,21 @@ describe('search_bar', () => { title: 'test-index', }) ); + + dispatchSpy = jest.fn(store.dispatch); + store.dispatch = dispatchSpy; }); async function mountSearchBar() { jest.clearAllMocks(); - const wrappedSearchBar = wrapSearchBarInContext({ ...defaultProps }); + const searchBarTestRoot = React.createElement((updatedProps: SearchBarProps) => ( + + {wrapSearchBarInContext({ ...defaultProps, ...updatedProps })} + + )); await act(async () => { - instance = mountWithIntl({wrappedSearchBar}); + instance = mountWithIntl(searchBarTestRoot); }); } @@ -119,7 +139,10 @@ describe('search_bar', () => { instance.find('form').simulate('submit', { preventDefault: () => {} }); }); - expect(defaultProps.onQuerySubmit).toHaveBeenCalledWith('testQuery'); + expect(dispatchSpy).toHaveBeenCalledWith({ + type: 'x-pack/graph/workspace/SUBMIT_SEARCH', + payload: 'testQuery', + }); }); it('should translate kql query into JSON dsl', async () => { @@ -135,7 +158,7 @@ describe('search_bar', () => { instance.find('form').simulate('submit', { preventDefault: () => {} }); }); - const parsedQuery = JSON.parse(defaultProps.onQuerySubmit.mock.calls[0][0]); + const parsedQuery = JSON.parse(dispatchSpy.mock.calls[0][0].payload); expect(parsedQuery).toEqual({ bool: { should: [{ match: { test: 'abc' } }], minimum_should_match: 1 }, }); diff --git a/x-pack/plugins/graph/public/components/search_bar.tsx b/x-pack/plugins/graph/public/components/search_bar.tsx index fdf198c761957..fc7e3be3d0d37 100644 --- a/x-pack/plugins/graph/public/components/search_bar.tsx +++ b/x-pack/plugins/graph/public/components/search_bar.tsx @@ -17,6 +17,7 @@ import { datasourceSelector, requestDatasource, IndexpatternDatasource, + submitSearch, } from '../state_management'; import { useKibana } from '../../../../../src/plugins/kibana_react/public'; @@ -28,11 +29,11 @@ import { esKuery, } from '../../../../../src/plugins/data/public'; -export interface OuterSearchBarProps { +export interface SearchBarProps { isLoading: boolean; - initialQuery?: string; - onQuerySubmit: (query: string) => void; - + urlQuery: string | null; + currentIndexPattern?: IndexPattern; + onIndexPatternChange: (indexPattern?: IndexPattern) => void; confirmWipeWorkspace: ( onConfirm: () => void, text?: string, @@ -41,9 +42,10 @@ export interface OuterSearchBarProps { indexPatternProvider: IndexPatternProvider; } -export interface SearchBarProps extends OuterSearchBarProps { +export interface SearchBarStateProps { currentDatasource?: IndexpatternDatasource; onIndexPatternSelected: (indexPattern: IndexPatternSavedObject) => void; + submit: (searchTerm: string) => void; } function queryToString(query: Query, indexPattern: IndexPattern) { @@ -65,31 +67,34 @@ function queryToString(query: Query, indexPattern: IndexPattern) { return JSON.stringify(query.query); } -export function SearchBarComponent(props: SearchBarProps) { +export function SearchBarComponent(props: SearchBarStateProps & SearchBarProps) { const { - currentDatasource, - onQuerySubmit, isLoading, - onIndexPatternSelected, - initialQuery, + urlQuery, + currentIndexPattern, + currentDatasource, indexPatternProvider, + submit, + onIndexPatternSelected, confirmWipeWorkspace, + onIndexPatternChange, } = props; - const [query, setQuery] = useState({ language: 'kuery', query: initialQuery || '' }); - const [currentIndexPattern, setCurrentIndexPattern] = useState( - undefined - ); + const [query, setQuery] = useState({ language: 'kuery', query: urlQuery || '' }); + + useEffect(() => setQuery((prev) => ({ language: prev.language, query: urlQuery || '' })), [ + urlQuery, + ]); useEffect(() => { async function fetchPattern() { if (currentDatasource) { - setCurrentIndexPattern(await indexPatternProvider.get(currentDatasource.id)); + onIndexPatternChange(await indexPatternProvider.get(currentDatasource.id)); } else { - setCurrentIndexPattern(undefined); + onIndexPatternChange(undefined); } } fetchPattern(); - }, [currentDatasource, indexPatternProvider]); + }, [currentDatasource, indexPatternProvider, onIndexPatternChange]); const kibana = useKibana(); const { services, overlays } = kibana; @@ -101,7 +106,7 @@ export function SearchBarComponent(props: SearchBarProps) { onSubmit={(e) => { e.preventDefault(); if (!isLoading && currentIndexPattern) { - onQuerySubmit(queryToString(query, currentIndexPattern)); + submit(queryToString(query, currentIndexPattern)); } }} > @@ -196,5 +201,8 @@ export const SearchBar = connect( }) ); }, + submit: (searchTerm: string) => { + dispatch(submitSearch(searchTerm)); + }, }) )(SearchBarComponent); diff --git a/x-pack/plugins/graph/public/components/settings/advanced_settings_form.tsx b/x-pack/plugins/graph/public/components/settings/advanced_settings_form.tsx index 10ee306cd48a2..44ce606b0c1a9 100644 --- a/x-pack/plugins/graph/public/components/settings/advanced_settings_form.tsx +++ b/x-pack/plugins/graph/public/components/settings/advanced_settings_form.tsx @@ -8,8 +8,8 @@ import React, { useState, useEffect } from 'react'; import { EuiFormRow, EuiFieldNumber, EuiComboBox, EuiSwitch, EuiText } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { SettingsProps } from './settings'; import { AdvancedSettings } from '../../types'; +import { SettingsStateProps } from './settings'; // Helper type to get all keys of an interface // that are of type number. @@ -26,9 +26,10 @@ export function AdvancedSettingsForm({ advancedSettings, updateSettings, allFields, -}: Pick) { +}: Pick) { // keep a local state during changes const [formState, updateFormState] = useState({ ...advancedSettings }); + // useEffect update localState only based on the main store useEffect(() => { updateFormState(advancedSettings); diff --git a/x-pack/plugins/graph/public/components/settings/blocklist_form.tsx b/x-pack/plugins/graph/public/components/settings/blocklist_form.tsx index 6f6b759f1ee1b..8954e812bdb88 100644 --- a/x-pack/plugins/graph/public/components/settings/blocklist_form.tsx +++ b/x-pack/plugins/graph/public/components/settings/blocklist_form.tsx @@ -17,14 +17,15 @@ import { EuiCallOut, } from '@elastic/eui'; -import { SettingsProps } from './settings'; +import { SettingsWorkspaceProps } from './settings'; import { LegacyIcon } from '../legacy_icon'; import { useListKeys } from './use_list_keys'; export function BlocklistForm({ blocklistedNodes, - unblocklistNode, -}: Pick) { + unblockNode, + unblockAll, +}: Pick) { const getListKey = useListKeys(blocklistedNodes || []); return ( <> @@ -46,7 +47,7 @@ export function BlocklistForm({ /> )} - {blocklistedNodes && unblocklistNode && blocklistedNodes.length > 0 && ( + {blocklistedNodes && blocklistedNodes.length > 0 && ( <> {blocklistedNodes.map((node) => ( @@ -63,9 +64,7 @@ export function BlocklistForm({ defaultMessage: 'Delete', }), color: 'danger', - onClick: () => { - unblocklistNode(node); - }, + onClick: () => unblockNode(node), }} /> ))} @@ -77,11 +76,7 @@ export function BlocklistForm({ iconType="trash" size="s" fill - onClick={() => { - blocklistedNodes.forEach((node) => { - unblocklistNode(node); - }); - }} + onClick={() => unblockAll()} > {i18n.translate('xpack.graph.settings.blocklist.clearButtonLabel', { defaultMessage: 'Delete all', diff --git a/x-pack/plugins/graph/public/components/settings/settings.test.tsx b/x-pack/plugins/graph/public/components/settings/settings.test.tsx index f0d506cf47556..060b1e93fbdc0 100644 --- a/x-pack/plugins/graph/public/components/settings/settings.test.tsx +++ b/x-pack/plugins/graph/public/components/settings/settings.test.tsx @@ -9,7 +9,7 @@ import React from 'react'; import { EuiTab, EuiListGroupItem, EuiButton, EuiAccordion, EuiFieldText } from '@elastic/eui'; import * as Rx from 'rxjs'; import { mountWithIntl } from '@kbn/test/jest'; -import { Settings, AngularProps } from './settings'; +import { Settings, SettingsWorkspaceProps } from './settings'; import { act } from '@testing-library/react'; import { ReactWrapper } from 'enzyme'; import { UrlTemplateForm } from './url_template_form'; @@ -46,7 +46,7 @@ describe('settings', () => { isDefault: false, }; - const angularProps: jest.Mocked = { + const workspaceProps: jest.Mocked = { blocklistedNodes: [ { x: 0, @@ -83,11 +83,12 @@ describe('settings', () => { }, }, ], - unblocklistNode: jest.fn(), + unblockNode: jest.fn(), + unblockAll: jest.fn(), canEditDrillDownUrls: true, }; - let subject: Rx.BehaviorSubject>; + let subject: Rx.BehaviorSubject>; let instance: ReactWrapper; beforeEach(() => { @@ -137,7 +138,7 @@ describe('settings', () => { ); dispatchSpy = jest.fn(store.dispatch); store.dispatch = dispatchSpy; - subject = new Rx.BehaviorSubject(angularProps); + subject = new Rx.BehaviorSubject(workspaceProps); instance = mountWithIntl( @@ -217,7 +218,7 @@ describe('settings', () => { it('should update on new data', () => { act(() => { subject.next({ - ...angularProps, + ...workspaceProps, blocklistedNodes: [ { x: 0, @@ -250,14 +251,13 @@ describe('settings', () => { it('should delete node', () => { instance.find(EuiListGroupItem).at(0).prop('extraAction')!.onClick!({} as any); - expect(angularProps.unblocklistNode).toHaveBeenCalledWith(angularProps.blocklistedNodes![0]); + expect(workspaceProps.unblockNode).toHaveBeenCalledWith(workspaceProps.blocklistedNodes![0]); }); it('should delete all nodes', () => { instance.find('[data-test-subj="graphUnblocklistAll"]').find(EuiButton).simulate('click'); - expect(angularProps.unblocklistNode).toHaveBeenCalledWith(angularProps.blocklistedNodes![0]); - expect(angularProps.unblocklistNode).toHaveBeenCalledWith(angularProps.blocklistedNodes![1]); + expect(workspaceProps.unblockAll).toHaveBeenCalled(); }); }); diff --git a/x-pack/plugins/graph/public/components/settings/settings.tsx b/x-pack/plugins/graph/public/components/settings/settings.tsx index ab9cfdfe38072..d8f18add4f375 100644 --- a/x-pack/plugins/graph/public/components/settings/settings.tsx +++ b/x-pack/plugins/graph/public/components/settings/settings.tsx @@ -6,7 +6,7 @@ */ import { i18n } from '@kbn/i18n'; -import React, { useState, useEffect } from 'react'; +import React, { useEffect, useState } from 'react'; import { EuiFlyoutHeader, EuiTitle, EuiTabs, EuiFlyoutBody, EuiTab } from '@elastic/eui'; import * as Rx from 'rxjs'; import { connect } from 'react-redux'; @@ -14,7 +14,7 @@ import { bindActionCreators } from 'redux'; import { AdvancedSettingsForm } from './advanced_settings_form'; import { BlocklistForm } from './blocklist_form'; import { UrlTemplateList } from './url_template_list'; -import { WorkspaceNode, AdvancedSettings, UrlTemplate, WorkspaceField } from '../../types'; +import { AdvancedSettings, BlockListedNode, UrlTemplate, WorkspaceField } from '../../types'; import { GraphState, settingsSelector, @@ -47,16 +47,6 @@ const tabs = [ }, ]; -/** - * These props are wired in the angular scope and are passed in via observable - * to catch update outside updates - */ -export interface AngularProps { - blocklistedNodes: WorkspaceNode[]; - unblocklistNode: (node: WorkspaceNode) => void; - canEditDrillDownUrls: boolean; -} - export interface StateProps { advancedSettings: AdvancedSettings; urlTemplates: UrlTemplate[]; @@ -69,26 +59,43 @@ export interface DispatchProps { saveTemplate: (props: { index: number; template: UrlTemplate }) => void; } -interface AsObservable

{ +export interface SettingsWorkspaceProps { + blocklistedNodes: BlockListedNode[]; + unblockNode: (node: BlockListedNode) => void; + unblockAll: () => void; + canEditDrillDownUrls: boolean; +} + +export interface AsObservable

{ observable: Readonly>; } -export interface SettingsProps extends AngularProps, StateProps, DispatchProps {} +export interface SettingsStateProps extends StateProps, DispatchProps {} export function SettingsComponent({ observable, - ...props -}: AsObservable & StateProps & DispatchProps) { - const [angularProps, setAngularProps] = useState(undefined); + advancedSettings, + urlTemplates, + allFields, + saveTemplate: saveTemplateAction, + updateSettings: updateSettingsAction, + removeTemplate: removeTemplateAction, +}: AsObservable & SettingsStateProps) { + const [workspaceProps, setWorkspaceProps] = useState( + undefined + ); const [activeTab, setActiveTab] = useState(0); useEffect(() => { - observable.subscribe(setAngularProps); + observable.subscribe(setWorkspaceProps); }, [observable]); - if (!angularProps) return null; + if (!workspaceProps) { + return null; + } const ActiveTabContent = tabs[activeTab].component; + return ( <> @@ -97,7 +104,7 @@ export function SettingsComponent({ {tabs - .filter(({ id }) => id !== 'drillDowns' || angularProps.canEditDrillDownUrls) + .filter(({ id }) => id !== 'drillDowns' || workspaceProps.canEditDrillDownUrls) .map(({ title }, index) => ( - + ); } -export const Settings = connect, GraphState>( +export const Settings = connect< + StateProps, + DispatchProps, + AsObservable, + GraphState +>( (state: GraphState) => ({ advancedSettings: settingsSelector(state), urlTemplates: templatesSelector(state), diff --git a/x-pack/plugins/graph/public/components/settings/url_template_list.tsx b/x-pack/plugins/graph/public/components/settings/url_template_list.tsx index 24ce9dd267ad0..d18a9adb9bc0d 100644 --- a/x-pack/plugins/graph/public/components/settings/url_template_list.tsx +++ b/x-pack/plugins/graph/public/components/settings/url_template_list.tsx @@ -8,7 +8,7 @@ import React, { useState } from 'react'; import { EuiText, EuiSpacer, EuiTextAlign, EuiButton, htmlIdGenerator } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { SettingsProps } from './settings'; +import { SettingsStateProps } from './settings'; import { UrlTemplateForm } from './url_template_form'; import { useListKeys } from './use_list_keys'; @@ -18,7 +18,7 @@ export function UrlTemplateList({ removeTemplate, saveTemplate, urlTemplates, -}: Pick) { +}: Pick) { const [uncommittedForms, setUncommittedForms] = useState([]); const getListKey = useListKeys(urlTemplates); diff --git a/x-pack/plugins/graph/public/components/workspace_layout/index.ts b/x-pack/plugins/graph/public/components/workspace_layout/index.ts new file mode 100644 index 0000000000000..9f753a5bad576 --- /dev/null +++ b/x-pack/plugins/graph/public/components/workspace_layout/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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export * from './workspace_layout'; diff --git a/x-pack/plugins/graph/public/components/workspace_layout/workspace_layout.tsx b/x-pack/plugins/graph/public/components/workspace_layout/workspace_layout.tsx new file mode 100644 index 0000000000000..70e5b82ec6526 --- /dev/null +++ b/x-pack/plugins/graph/public/components/workspace_layout/workspace_layout.tsx @@ -0,0 +1,234 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { Fragment, memo, useCallback, useRef, useState } from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiSpacer } from '@elastic/eui'; +import { connect } from 'react-redux'; +import { SearchBar } from '../search_bar'; +import { + GraphState, + hasFieldsSelector, + workspaceInitializedSelector, +} from '../../state_management'; +import { FieldManager } from '../field_manager'; +import { IndexPattern } from '../../../../../../src/plugins/data/public'; +import { + ControlType, + IndexPatternProvider, + IndexPatternSavedObject, + TermIntersect, + WorkspaceNode, +} from '../../types'; +import { WorkspaceTopNavMenu } from './workspace_top_nav_menu'; +import { InspectPanel } from '../inspect_panel'; +import { GuidancePanel } from '../guidance_panel'; +import { GraphTitle } from '../graph_title'; +import { GraphWorkspaceSavedObject, Workspace } from '../../types'; +import { GraphServices } from '../../application'; +import { ControlPanel } from '../control_panel'; +import { GraphVisualization } from '../graph_visualization'; +import { colorChoices } from '../../helpers/style_choices'; + +/** + * Each component, which depends on `worksapce` + * should not be memoized, since it will not get updates. + * This behaviour should be changed after migrating `worksapce` to redux + */ +const FieldManagerMemoized = memo(FieldManager); +const GuidancePanelMemoized = memo(GuidancePanel); + +type WorkspaceLayoutProps = Pick< + GraphServices, + | 'setHeaderActionMenu' + | 'graphSavePolicy' + | 'navigation' + | 'capabilities' + | 'coreStart' + | 'canEditDrillDownUrls' + | 'overlays' +> & { + renderCounter: number; + workspace?: Workspace; + loading: boolean; + indexPatterns: IndexPatternSavedObject[]; + savedWorkspace: GraphWorkspaceSavedObject; + indexPatternProvider: IndexPatternProvider; + urlQuery: string | null; +}; + +interface WorkspaceLayoutStateProps { + workspaceInitialized: boolean; + hasFields: boolean; +} + +const WorkspaceLayoutComponent = ({ + renderCounter, + workspace, + loading, + savedWorkspace, + hasFields, + overlays, + workspaceInitialized, + indexPatterns, + indexPatternProvider, + capabilities, + coreStart, + graphSavePolicy, + navigation, + canEditDrillDownUrls, + urlQuery, + setHeaderActionMenu, +}: WorkspaceLayoutProps & WorkspaceLayoutStateProps) => { + const [currentIndexPattern, setCurrentIndexPattern] = useState(); + const [showInspect, setShowInspect] = useState(false); + const [pickerOpen, setPickerOpen] = useState(false); + const [mergeCandidates, setMergeCandidates] = useState([]); + const [control, setControl] = useState('none'); + const selectedNode = useRef(undefined); + const isInitialized = Boolean(workspaceInitialized || savedWorkspace.id); + + const selectSelected = useCallback((node: WorkspaceNode) => { + selectedNode.current = node; + setControl('editLabel'); + }, []); + + const onSetControl = useCallback((newControl: ControlType) => { + selectedNode.current = undefined; + setControl(newControl); + }, []); + + const onIndexPatternChange = useCallback( + (indexPattern?: IndexPattern) => setCurrentIndexPattern(indexPattern), + [] + ); + + const onOpenFieldPicker = useCallback(() => { + setPickerOpen(true); + }, []); + + const confirmWipeWorkspace = useCallback( + ( + onConfirm: () => void, + text?: string, + options?: { confirmButtonText: string; title: string } + ) => { + if (!hasFields) { + onConfirm(); + return; + } + const confirmModalOptions = { + confirmButtonText: i18n.translate('xpack.graph.leaveWorkspace.confirmButtonLabel', { + defaultMessage: 'Leave anyway', + }), + title: i18n.translate('xpack.graph.leaveWorkspace.modalTitle', { + defaultMessage: 'Unsaved changes', + }), + 'data-test-subj': 'confirmModal', + ...options, + }; + + overlays + .openConfirm( + text || + i18n.translate('xpack.graph.leaveWorkspace.confirmText', { + defaultMessage: 'If you leave now, you will lose unsaved changes.', + }), + confirmModalOptions + ) + .then((isConfirmed) => { + if (isConfirmed) { + onConfirm(); + } + }); + }, + [hasFields, overlays] + ); + + const onSetMergeCandidates = useCallback( + (terms: TermIntersect[]) => setMergeCandidates(terms), + [] + ); + + return ( + + + + + + {isInitialized && } +

+ + + +
+ {!isInitialized && ( +
+ +
+ )} + + {isInitialized && workspace && ( +
+
+ +
+ + +
+ )} + + ); +}; + +export const WorkspaceLayout = connect( + (state: GraphState) => ({ + workspaceInitialized: workspaceInitializedSelector(state), + hasFields: hasFieldsSelector(state), + }) +)(WorkspaceLayoutComponent); diff --git a/x-pack/plugins/graph/public/components/workspace_layout/workspace_top_nav_menu.tsx b/x-pack/plugins/graph/public/components/workspace_layout/workspace_top_nav_menu.tsx new file mode 100644 index 0000000000000..c5b10b9d92120 --- /dev/null +++ b/x-pack/plugins/graph/public/components/workspace_layout/workspace_top_nav_menu.tsx @@ -0,0 +1,175 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { Provider, useStore } from 'react-redux'; +import { AppMountParameters, Capabilities, CoreStart } from 'kibana/public'; +import { useHistory, useLocation } from 'react-router-dom'; +import { NavigationPublicPluginStart as NavigationStart } from '../../../../../../src/plugins/navigation/public'; +import { toMountPoint } from '../../../../../../src/plugins/kibana_react/public'; +import { datasourceSelector, hasFieldsSelector } from '../../state_management'; +import { GraphSavePolicy, GraphWorkspaceSavedObject, Workspace } from '../../types'; +import { AsObservable, Settings, SettingsWorkspaceProps } from '../settings'; +import { asSyncedObservable } from '../../helpers/as_observable'; + +interface WorkspaceTopNavMenuProps { + workspace: Workspace | undefined; + setShowInspect: React.Dispatch>; + confirmWipeWorkspace: ( + onConfirm: () => void, + text?: string, + options?: { confirmButtonText: string; title: string } + ) => void; + savedWorkspace: GraphWorkspaceSavedObject; + setHeaderActionMenu: AppMountParameters['setHeaderActionMenu']; + graphSavePolicy: GraphSavePolicy; + navigation: NavigationStart; + capabilities: Capabilities; + coreStart: CoreStart; + canEditDrillDownUrls: boolean; + isInitialized: boolean; +} + +export const WorkspaceTopNavMenu = (props: WorkspaceTopNavMenuProps) => { + const store = useStore(); + const location = useLocation(); + const history = useHistory(); + + // register things for legacy angular UI + const allSavingDisabled = props.graphSavePolicy === 'none'; + + // ===== Menubar configuration ========= + const { TopNavMenu } = props.navigation.ui; + const topNavMenu = []; + topNavMenu.push({ + key: 'new', + label: i18n.translate('xpack.graph.topNavMenu.newWorkspaceLabel', { + defaultMessage: 'New', + }), + description: i18n.translate('xpack.graph.topNavMenu.newWorkspaceAriaLabel', { + defaultMessage: 'New Workspace', + }), + tooltip: i18n.translate('xpack.graph.topNavMenu.newWorkspaceTooltip', { + defaultMessage: 'Create a new workspace', + }), + disableButton() { + return !props.isInitialized; + }, + run() { + props.confirmWipeWorkspace(() => { + if (location.pathname === '/workspace/') { + history.go(0); + } else { + history.push('/workspace/'); + } + }); + }, + testId: 'graphNewButton', + }); + + // if saving is disabled using uiCapabilities, we don't want to render the save + // button so it's consistent with all of the other applications + if (props.capabilities.graph.save) { + // allSavingDisabled is based on the xpack.graph.savePolicy, we'll maintain this functionality + + topNavMenu.push({ + key: 'save', + label: i18n.translate('xpack.graph.topNavMenu.saveWorkspace.enabledLabel', { + defaultMessage: 'Save', + }), + description: i18n.translate('xpack.graph.topNavMenu.saveWorkspace.enabledAriaLabel', { + defaultMessage: 'Save workspace', + }), + tooltip: () => { + if (allSavingDisabled) { + return i18n.translate('xpack.graph.topNavMenu.saveWorkspace.disabledTooltip', { + defaultMessage: + 'No changes to saved workspaces are permitted by the current save policy', + }); + } else { + return i18n.translate('xpack.graph.topNavMenu.saveWorkspace.enabledTooltip', { + defaultMessage: 'Save this workspace', + }); + } + }, + disableButton() { + return allSavingDisabled || !hasFieldsSelector(store.getState()); + }, + run: () => { + store.dispatch({ type: 'x-pack/graph/SAVE_WORKSPACE', payload: props.savedWorkspace }); + }, + testId: 'graphSaveButton', + }); + } + topNavMenu.push({ + key: 'inspect', + disableButton() { + return props.workspace === null; + }, + label: i18n.translate('xpack.graph.topNavMenu.inspectLabel', { + defaultMessage: 'Inspect', + }), + description: i18n.translate('xpack.graph.topNavMenu.inspectAriaLabel', { + defaultMessage: 'Inspect', + }), + run: () => { + props.setShowInspect((prevShowInspect) => !prevShowInspect); + }, + }); + + topNavMenu.push({ + key: 'settings', + disableButton() { + return datasourceSelector(store.getState()).current.type === 'none'; + }, + label: i18n.translate('xpack.graph.topNavMenu.settingsLabel', { + defaultMessage: 'Settings', + }), + description: i18n.translate('xpack.graph.topNavMenu.settingsAriaLabel', { + defaultMessage: 'Settings', + }), + run: () => { + // At this point workspace should be initialized, + // since settings button will be disabled only if workspace was set + const workspace = props.workspace as Workspace; + + const settingsObservable = (asSyncedObservable(() => ({ + blocklistedNodes: workspace.blocklistedNodes, + unblockNode: workspace.unblockNode, + unblockAll: workspace.unblockAll, + canEditDrillDownUrls: props.canEditDrillDownUrls, + })) as unknown) as AsObservable['observable']; + + props.coreStart.overlays.openFlyout( + toMountPoint( + + + + ), + { + size: 'm', + closeButtonAriaLabel: i18n.translate('xpack.graph.settings.closeLabel', { + defaultMessage: 'Close', + }), + 'data-test-subj': 'graphSettingsFlyout', + ownFocus: true, + className: 'gphSettingsFlyout', + maxWidth: 520, + } + ); + }, + }); + + return ( + + ); +}; diff --git a/x-pack/plugins/graph/public/helpers/as_observable.ts b/x-pack/plugins/graph/public/helpers/as_observable.ts index c1fa963641366..146161cceb46d 100644 --- a/x-pack/plugins/graph/public/helpers/as_observable.ts +++ b/x-pack/plugins/graph/public/helpers/as_observable.ts @@ -12,19 +12,20 @@ interface Props { } /** - * This is a helper to tie state updates that happen somewhere else back to an angular scope. + * This is a helper to tie state updates that happen somewhere else back to an react state. * It is roughly comparable to `reactDirective`, but does not have to be used from within a * template. * - * This is a temporary solution until the state management is moved outside of Angular. + * This is a temporary solution until the state of Workspace internals is moved outside + * of mutable object to the redux state (at least blocklistedNodes, canEditDrillDownUrls and + * unblocklist action in this case). * * @param collectProps Function that collects properties from the scope that should be passed - * into the observable. All functions passed along will be wrapped to cause an angular digest cycle - * and refresh the observable afterwards with a new call to `collectProps`. By doing so, angular - * can react to changes made outside of it and the results are passed back via the observable - * @param angularDigest The `$digest` function of the scope. + * into the observable. All functions passed along will be wrapped to cause a react render + * and refresh the observable afterwards with a new call to `collectProps`. By doing so, react + * will receive an update outside of it local state and the results are passed back via the observable. */ -export function asAngularSyncedObservable(collectProps: () => Props, angularDigest: () => void) { +export function asSyncedObservable(collectProps: () => Props) { const boundCollectProps = () => { const collectedProps = collectProps(); Object.keys(collectedProps).forEach((key) => { @@ -32,7 +33,6 @@ export function asAngularSyncedObservable(collectProps: () => Props, angularDige if (typeof currentValue === 'function') { collectedProps[key] = (...args: unknown[]) => { currentValue(...args); - angularDigest(); subject$.next(boundCollectProps()); }; } diff --git a/x-pack/plugins/graph/public/helpers/saved_workspace_utils.ts b/x-pack/plugins/graph/public/helpers/saved_workspace_utils.ts index 1d8be0fe86b97..336708173d321 100644 --- a/x-pack/plugins/graph/public/helpers/saved_workspace_utils.ts +++ b/x-pack/plugins/graph/public/helpers/saved_workspace_utils.ts @@ -49,7 +49,7 @@ const defaultsProps = { const urlFor = (basePath: IBasePath, id: string) => basePath.prepend(`/app/graph#/workspace/${encodeURIComponent(id)}`); -function mapHits(hit: { id: string; attributes: Record }, url: string) { +function mapHits(hit: any, url: string): GraphWorkspaceSavedObject { const source = hit.attributes; source.id = hit.id; source.url = url; diff --git a/x-pack/plugins/graph/public/helpers/use_graph_loader.ts b/x-pack/plugins/graph/public/helpers/use_graph_loader.ts new file mode 100644 index 0000000000000..c133f6bf260cd --- /dev/null +++ b/x-pack/plugins/graph/public/helpers/use_graph_loader.ts @@ -0,0 +1,108 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useCallback, useState } from 'react'; +import { ToastsStart } from 'kibana/public'; +import { IHttpFetchError, CoreStart } from 'kibana/public'; +import { i18n } from '@kbn/i18n'; +import { ExploreRequest, GraphExploreCallback, GraphSearchCallback, SearchRequest } from '../types'; +import { formatHttpError } from './format_http_error'; + +interface UseGraphLoaderProps { + toastNotifications: ToastsStart; + coreStart: CoreStart; +} + +export const useGraphLoader = ({ toastNotifications, coreStart }: UseGraphLoaderProps) => { + const [loading, setLoading] = useState(false); + + const handleHttpError = useCallback( + (error: IHttpFetchError) => { + toastNotifications.addDanger(formatHttpError(error)); + }, + [toastNotifications] + ); + + const handleSearchQueryError = useCallback( + (err: Error | string) => { + const toastTitle = i18n.translate('xpack.graph.errorToastTitle', { + defaultMessage: 'Graph Error', + description: '"Graph" is a product name and should not be translated.', + }); + if (err instanceof Error) { + toastNotifications.addError(err, { + title: toastTitle, + }); + } else { + toastNotifications.addDanger({ + title: toastTitle, + text: String(err), + }); + } + }, + [toastNotifications] + ); + + // Replacement function for graphClientWorkspace's comms so + // that it works with Kibana. + const callNodeProxy = useCallback( + (indexName: string, query: ExploreRequest, responseHandler: GraphExploreCallback) => { + const request = { + body: JSON.stringify({ + index: indexName, + query, + }), + }; + setLoading(true); + return coreStart.http + .post('../api/graph/graphExplore', request) + .then(function (data) { + const response = data.resp; + if (response.timed_out) { + toastNotifications.addWarning( + i18n.translate('xpack.graph.exploreGraph.timedOutWarningText', { + defaultMessage: 'Exploration timed out', + }) + ); + } + responseHandler(response); + }) + .catch(handleHttpError) + .finally(() => setLoading(false)); + }, + [coreStart.http, handleHttpError, toastNotifications] + ); + + // Helper function for the graphClientWorkspace to perform a query + const callSearchNodeProxy = useCallback( + (indexName: string, query: SearchRequest, responseHandler: GraphSearchCallback) => { + const request = { + body: JSON.stringify({ + index: indexName, + body: query, + }), + }; + setLoading(true); + coreStart.http + .post('../api/graph/searchProxy', request) + .then(function (data) { + const response = data.resp; + responseHandler(response); + }) + .catch(handleHttpError) + .finally(() => setLoading(false)); + }, + [coreStart.http, handleHttpError] + ); + + return { + loading, + callNodeProxy, + callSearchNodeProxy, + handleSearchQueryError, + }; +}; diff --git a/x-pack/plugins/graph/public/helpers/use_workspace_loader.ts b/x-pack/plugins/graph/public/helpers/use_workspace_loader.ts new file mode 100644 index 0000000000000..8b91546d52446 --- /dev/null +++ b/x-pack/plugins/graph/public/helpers/use_workspace_loader.ts @@ -0,0 +1,120 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { SavedObjectsClientContract, ToastsStart } from 'kibana/public'; +import { useEffect, useState } from 'react'; +import { useHistory, useLocation, useParams } from 'react-router-dom'; +import { i18n } from '@kbn/i18n'; +import { GraphStore } from '../state_management'; +import { GraphWorkspaceSavedObject, IndexPatternSavedObject, Workspace } from '../types'; +import { getSavedWorkspace } from './saved_workspace_utils'; + +interface UseWorkspaceLoaderProps { + store: GraphStore; + workspaceRef: React.MutableRefObject; + savedObjectsClient: SavedObjectsClientContract; + toastNotifications: ToastsStart; +} + +interface WorkspaceUrlParams { + id?: string; +} + +export const useWorkspaceLoader = ({ + workspaceRef, + store, + savedObjectsClient, + toastNotifications, +}: UseWorkspaceLoaderProps) => { + const [indexPatterns, setIndexPatterns] = useState(); + const [savedWorkspace, setSavedWorkspace] = useState(); + const history = useHistory(); + const location = useLocation(); + const { id } = useParams(); + + /** + * The following effect initializes workspace initially and reacts + * on changes in id parameter and URL query only. + */ + useEffect(() => { + const urlQuery = new URLSearchParams(location.search).get('query'); + + function loadWorkspace( + fetchedSavedWorkspace: GraphWorkspaceSavedObject, + fetchedIndexPatterns: IndexPatternSavedObject[] + ) { + store.dispatch({ + type: 'x-pack/graph/LOAD_WORKSPACE', + payload: { + savedWorkspace: fetchedSavedWorkspace, + indexPatterns: fetchedIndexPatterns, + urlQuery, + }, + }); + } + + function clearStore() { + store.dispatch({ type: 'x-pack/graph/RESET' }); + } + + async function fetchIndexPatterns() { + return await savedObjectsClient + .find<{ title: string }>({ + type: 'index-pattern', + fields: ['title', 'type'], + perPage: 10000, + }) + .then((response) => response.savedObjects); + } + + async function fetchSavedWorkspace() { + return (id + ? await getSavedWorkspace(savedObjectsClient, id).catch(function (e) { + toastNotifications.addError(e, { + title: i18n.translate('xpack.graph.missingWorkspaceErrorMessage', { + defaultMessage: "Couldn't load graph with ID", + }), + }); + history.replace('/home'); + // return promise that never returns to prevent the controller from loading + return new Promise(() => {}); + }) + : await getSavedWorkspace(savedObjectsClient)) as GraphWorkspaceSavedObject; + } + + async function initializeWorkspace() { + const fetchedIndexPatterns = await fetchIndexPatterns(); + const fetchedSavedWorkspace = await fetchSavedWorkspace(); + + /** + * Deal with situation of request to open saved workspace. Otherwise clean up store, + * when navigating to a new workspace from existing one. + */ + if (fetchedSavedWorkspace.id) { + loadWorkspace(fetchedSavedWorkspace, fetchedIndexPatterns); + } else if (workspaceRef.current) { + clearStore(); + } + + setIndexPatterns(fetchedIndexPatterns); + setSavedWorkspace(fetchedSavedWorkspace); + } + + initializeWorkspace(); + }, [ + id, + location, + store, + history, + savedObjectsClient, + setSavedWorkspace, + toastNotifications, + workspaceRef, + ]); + + return { savedWorkspace, indexPatterns }; +}; diff --git a/x-pack/plugins/graph/public/index.scss b/x-pack/plugins/graph/public/index.scss index f4e38de3e93a4..4062864dd41e0 100644 --- a/x-pack/plugins/graph/public/index.scss +++ b/x-pack/plugins/graph/public/index.scss @@ -10,5 +10,4 @@ @import './mixins'; @import './main'; -@import './angular/templates/index'; @import './components/index'; diff --git a/x-pack/plugins/graph/public/plugin.ts b/x-pack/plugins/graph/public/plugin.ts index 70671260ce5b9..1ff9afe505a3b 100644 --- a/x-pack/plugins/graph/public/plugin.ts +++ b/x-pack/plugins/graph/public/plugin.ts @@ -84,7 +84,6 @@ export class GraphPlugin updater$: this.appUpdater$, mount: async (params: AppMountParameters) => { const [coreStart, pluginsStart] = await core.getStartServices(); - await pluginsStart.kibanaLegacy.loadAngularBootstrap(); coreStart.chrome.docTitle.change( i18n.translate('xpack.graph.pageTitle', { defaultMessage: 'Graph' }) ); @@ -104,7 +103,7 @@ export class GraphPlugin canEditDrillDownUrls: config.canEditDrillDownUrls, graphSavePolicy: config.savePolicy, storage: new Storage(window.localStorage), - capabilities: coreStart.application.capabilities.graph, + capabilities: coreStart.application.capabilities, chrome: coreStart.chrome, toastNotifications: coreStart.notifications.toasts, indexPatterns: pluginsStart.data!.indexPatterns, diff --git a/x-pack/plugins/graph/public/router.tsx b/x-pack/plugins/graph/public/router.tsx new file mode 100644 index 0000000000000..61a39bbbf63dd --- /dev/null +++ b/x-pack/plugins/graph/public/router.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { createHashHistory } from 'history'; +import { Redirect, Route, Router, Switch } from 'react-router-dom'; +import { ListingRoute } from './apps/listing_route'; +import { GraphServices } from './application'; +import { WorkspaceRoute } from './apps/workspace_route'; + +export const graphRouter = (deps: GraphServices) => { + const history = createHashHistory(); + + return ( + + + + + + + + + + + + + + ); +}; diff --git a/x-pack/plugins/graph/public/services/persistence/deserialize.test.ts b/x-pack/plugins/graph/public/services/persistence/deserialize.test.ts index 443d8581c435d..31826c3b3a747 100644 --- a/x-pack/plugins/graph/public/services/persistence/deserialize.test.ts +++ b/x-pack/plugins/graph/public/services/persistence/deserialize.test.ts @@ -7,7 +7,7 @@ import { GraphWorkspaceSavedObject, IndexPatternSavedObject, Workspace } from '../../types'; import { migrateLegacyIndexPatternRef, savedWorkspaceToAppState, mapFields } from './deserialize'; -import { createWorkspace } from '../../angular/graph_client_workspace'; +import { createWorkspace } from '../../services/workspace/graph_client_workspace'; import { outlinkEncoders } from '../../helpers/outlink_encoders'; import { IndexPattern } from '../../../../../../src/plugins/data/public'; diff --git a/x-pack/plugins/graph/public/services/persistence/serialize.test.ts b/x-pack/plugins/graph/public/services/persistence/serialize.test.ts index 8213aac3fd62e..2466582bc7b25 100644 --- a/x-pack/plugins/graph/public/services/persistence/serialize.test.ts +++ b/x-pack/plugins/graph/public/services/persistence/serialize.test.ts @@ -146,7 +146,7 @@ describe('serialize', () => { target: appState.workspace.nodes[0], weight: 5, width: 5, - }); + } as WorkspaceEdge); // C <-> E appState.workspace.edges.push({ @@ -155,7 +155,7 @@ describe('serialize', () => { target: appState.workspace.nodes[4], weight: 5, width: 5, - }); + } as WorkspaceEdge); }); it('should serialize given workspace', () => { diff --git a/x-pack/plugins/graph/public/services/persistence/serialize.ts b/x-pack/plugins/graph/public/services/persistence/serialize.ts index 65392b69b5a6e..e1ec8db19a4c4 100644 --- a/x-pack/plugins/graph/public/services/persistence/serialize.ts +++ b/x-pack/plugins/graph/public/services/persistence/serialize.ts @@ -6,7 +6,6 @@ */ import { - SerializedNode, WorkspaceNode, WorkspaceEdge, SerializedEdge, @@ -17,13 +16,15 @@ import { SerializedWorkspaceState, Workspace, AdvancedSettings, + SerializedNode, + BlockListedNode, } from '../../types'; import { IndexpatternDatasource } from '../../state_management'; function serializeNode( - { data, scaledSize, parent, x, y, label, color }: WorkspaceNode, + { data, scaledSize, parent, x, y, label, color }: BlockListedNode, allNodes: WorkspaceNode[] = [] -): SerializedNode { +) { return { x, y, diff --git a/x-pack/plugins/graph/public/services/save_modal.tsx b/x-pack/plugins/graph/public/services/save_modal.tsx index eff98ebeded47..f1603ed790d3a 100644 --- a/x-pack/plugins/graph/public/services/save_modal.tsx +++ b/x-pack/plugins/graph/public/services/save_modal.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React from 'react'; +import React, { ReactElement } from 'react'; import { I18nStart, OverlayStart, SavedObjectsClientContract } from 'src/core/public'; import { SaveResult } from 'src/plugins/saved_objects/public'; import { GraphWorkspaceSavedObject, GraphSavePolicy } from '../types'; @@ -39,7 +39,7 @@ export function openSaveModal({ hasData: boolean; workspace: GraphWorkspaceSavedObject; saveWorkspace: SaveWorkspaceHandler; - showSaveModal: (el: React.ReactNode, I18nContext: I18nStart['Context']) => void; + showSaveModal: (el: ReactElement, I18nContext: I18nStart['Context']) => void; I18nContext: I18nStart['Context']; services: SaveWorkspaceServices; }) { diff --git a/x-pack/plugins/graph/public/angular/graph_client_workspace.d.ts b/x-pack/plugins/graph/public/services/workspace/graph_client_workspace.d.ts similarity index 100% rename from x-pack/plugins/graph/public/angular/graph_client_workspace.d.ts rename to x-pack/plugins/graph/public/services/workspace/graph_client_workspace.d.ts diff --git a/x-pack/plugins/graph/public/angular/graph_client_workspace.js b/x-pack/plugins/graph/public/services/workspace/graph_client_workspace.js similarity index 99% rename from x-pack/plugins/graph/public/angular/graph_client_workspace.js rename to x-pack/plugins/graph/public/services/workspace/graph_client_workspace.js index 07e4dfc2e874a..c849a25cb19bb 100644 --- a/x-pack/plugins/graph/public/angular/graph_client_workspace.js +++ b/x-pack/plugins/graph/public/services/workspace/graph_client_workspace.js @@ -631,10 +631,14 @@ function GraphWorkspace(options) { self.runLayout(); }; - this.unblocklist = function (node) { + this.unblockNode = function (node) { self.arrRemove(self.blocklistedNodes, node); }; + this.unblockAll = function () { + self.arrRemoveAll(self.blocklistedNodes, self.blocklistedNodes); + }; + this.blocklistSelection = function () { const selection = self.getAllSelectedNodes(); const danglingEdges = []; diff --git a/x-pack/plugins/graph/public/angular/graph_client_workspace.test.js b/x-pack/plugins/graph/public/services/workspace/graph_client_workspace.test.js similarity index 100% rename from x-pack/plugins/graph/public/angular/graph_client_workspace.test.js rename to x-pack/plugins/graph/public/services/workspace/graph_client_workspace.test.js diff --git a/x-pack/plugins/graph/public/state_management/advanced_settings.ts b/x-pack/plugins/graph/public/state_management/advanced_settings.ts index 82f1358dd4164..68b9e002766e3 100644 --- a/x-pack/plugins/graph/public/state_management/advanced_settings.ts +++ b/x-pack/plugins/graph/public/state_management/advanced_settings.ts @@ -43,14 +43,14 @@ export const settingsSelector = (state: GraphState) => state.advancedSettings; * * Won't be necessary once the workspace is moved to redux */ -export const syncSettingsSaga = ({ getWorkspace, notifyAngular }: GraphStoreDependencies) => { +export const syncSettingsSaga = ({ getWorkspace, notifyReact }: GraphStoreDependencies) => { function* syncSettings(action: Action): IterableIterator { const workspace = getWorkspace(); if (!workspace) { return; } workspace.options.exploreControls = action.payload; - notifyAngular(); + notifyReact(); } return function* () { diff --git a/x-pack/plugins/graph/public/state_management/datasource.sagas.ts b/x-pack/plugins/graph/public/state_management/datasource.sagas.ts index b185af28c3481..9bfc7b3da0f91 100644 --- a/x-pack/plugins/graph/public/state_management/datasource.sagas.ts +++ b/x-pack/plugins/graph/public/state_management/datasource.sagas.ts @@ -30,7 +30,7 @@ export const datasourceSaga = ({ indexPatternProvider, notifications, createWorkspace, - notifyAngular, + notifyReact, }: GraphStoreDependencies) => { function* fetchFields(action: Action) { try { @@ -39,7 +39,7 @@ export const datasourceSaga = ({ yield put(datasourceLoaded()); const advancedSettings = settingsSelector(yield select()); createWorkspace(indexPattern.title, advancedSettings); - notifyAngular(); + notifyReact(); } catch (e) { // in case of errors, reset the datasource and show notification yield put(setDatasource({ type: 'none' })); diff --git a/x-pack/plugins/graph/public/state_management/fields.ts b/x-pack/plugins/graph/public/state_management/fields.ts index 051f5328091e1..3a117fa6fe50a 100644 --- a/x-pack/plugins/graph/public/state_management/fields.ts +++ b/x-pack/plugins/graph/public/state_management/fields.ts @@ -69,9 +69,9 @@ export const hasFieldsSelector = createSelector( * * Won't be necessary once the workspace is moved to redux */ -export const updateSaveButtonSaga = ({ notifyAngular }: GraphStoreDependencies) => { +export const updateSaveButtonSaga = ({ notifyReact }: GraphStoreDependencies) => { function* notify(): IterableIterator { - notifyAngular(); + notifyReact(); } return function* () { yield takeLatest(matchesOne(selectField, deselectField), notify); @@ -84,7 +84,7 @@ export const updateSaveButtonSaga = ({ notifyAngular }: GraphStoreDependencies) * * Won't be necessary once the workspace is moved to redux */ -export const syncFieldsSaga = ({ getWorkspace, setLiveResponseFields }: GraphStoreDependencies) => { +export const syncFieldsSaga = ({ getWorkspace }: GraphStoreDependencies) => { function* syncFields() { const workspace = getWorkspace(); if (!workspace) { @@ -93,7 +93,6 @@ export const syncFieldsSaga = ({ getWorkspace, setLiveResponseFields }: GraphSto const currentState = yield select(); workspace.options.vertex_fields = selectedFieldsSelector(currentState); - setLiveResponseFields(liveResponseFieldsSelector(currentState)); } return function* () { yield takeEvery( @@ -109,7 +108,7 @@ export const syncFieldsSaga = ({ getWorkspace, setLiveResponseFields }: GraphSto * * Won't be necessary once the workspace is moved to redux */ -export const syncNodeStyleSaga = ({ getWorkspace, notifyAngular }: GraphStoreDependencies) => { +export const syncNodeStyleSaga = ({ getWorkspace, notifyReact }: GraphStoreDependencies) => { function* syncNodeStyle(action: Action>) { const workspace = getWorkspace(); if (!workspace) { @@ -132,7 +131,7 @@ export const syncNodeStyleSaga = ({ getWorkspace, notifyAngular }: GraphStoreDep } }); } - notifyAngular(); + notifyReact(); const selectedFields = selectedFieldsSelector(yield select()); workspace.options.vertex_fields = selectedFields; diff --git a/x-pack/plugins/graph/public/state_management/legacy.test.ts b/x-pack/plugins/graph/public/state_management/legacy.test.ts index 1dbad39a918a5..5a05efdc478fc 100644 --- a/x-pack/plugins/graph/public/state_management/legacy.test.ts +++ b/x-pack/plugins/graph/public/state_management/legacy.test.ts @@ -77,13 +77,12 @@ describe('legacy sync sagas', () => { it('syncs templates with workspace', () => { env.store.dispatch(loadTemplates([])); - expect(env.mockedDeps.setUrlTemplates).toHaveBeenCalledWith([]); - expect(env.mockedDeps.notifyAngular).toHaveBeenCalled(); + expect(env.mockedDeps.notifyReact).toHaveBeenCalled(); }); it('notifies angular when fields are selected', () => { env.store.dispatch(selectField('field1')); - expect(env.mockedDeps.notifyAngular).toHaveBeenCalled(); + expect(env.mockedDeps.notifyReact).toHaveBeenCalled(); }); it('syncs field list with workspace', () => { @@ -99,9 +98,6 @@ describe('legacy sync sagas', () => { const workspace = env.mockedDeps.getWorkspace()!; expect(workspace.options.vertex_fields![0].name).toEqual('field1'); expect(workspace.options.vertex_fields![0].hopSize).toEqual(22); - expect(env.mockedDeps.setLiveResponseFields).toHaveBeenCalledWith([ - expect.objectContaining({ hopSize: 22 }), - ]); }); it('syncs styles with nodes', () => { diff --git a/x-pack/plugins/graph/public/state_management/mocks.ts b/x-pack/plugins/graph/public/state_management/mocks.ts index 74d980753a09a..189875d04b015 100644 --- a/x-pack/plugins/graph/public/state_management/mocks.ts +++ b/x-pack/plugins/graph/public/state_management/mocks.ts @@ -15,7 +15,7 @@ import createSagaMiddleware from 'redux-saga'; import { createStore, applyMiddleware, AnyAction } from 'redux'; import { ChromeStart } from 'kibana/public'; import { GraphStoreDependencies, createRootReducer, GraphStore, GraphState } from './store'; -import { Workspace, GraphWorkspaceSavedObject, IndexPatternSavedObject } from '../types'; +import { Workspace } from '../types'; import { IndexPattern } from '../../../../../src/plugins/data/public'; export interface MockedGraphEnvironment { @@ -48,11 +48,8 @@ export function createMockGraphStore({ blocklistedNodes: [], } as unknown) as Workspace; - const savedWorkspace = ({ - save: jest.fn(), - } as unknown) as GraphWorkspaceSavedObject; - const mockedDeps: jest.Mocked = { + basePath: '', addBasePath: jest.fn((url: string) => url), changeUrl: jest.fn(), chrome: ({ @@ -60,15 +57,11 @@ export function createMockGraphStore({ } as unknown) as ChromeStart, createWorkspace: jest.fn(), getWorkspace: jest.fn(() => workspaceMock), - getSavedWorkspace: jest.fn(() => savedWorkspace), indexPatternProvider: { get: jest.fn(() => Promise.resolve(({ id: '123', title: 'test-pattern' } as unknown) as IndexPattern) ), }, - indexPatterns: [ - ({ id: '123', attributes: { title: 'test-pattern' } } as unknown) as IndexPatternSavedObject, - ], I18nContext: jest .fn() .mockImplementation(({ children }: { children: React.ReactNode }) => children), @@ -79,12 +72,9 @@ export function createMockGraphStore({ }, } as unknown) as NotificationsStart, http: {} as HttpStart, - notifyAngular: jest.fn(), + notifyReact: jest.fn(), savePolicy: 'configAndData', showSaveModal: jest.fn(), - setLiveResponseFields: jest.fn(), - setUrlTemplates: jest.fn(), - setWorkspaceInitialized: jest.fn(), overlays: ({ openModal: jest.fn(), } as unknown) as OverlayStart, @@ -92,6 +82,7 @@ export function createMockGraphStore({ find: jest.fn(), get: jest.fn(), } as unknown) as SavedObjectsClientContract, + handleSearchQueryError: jest.fn(), ...mockedDepsOverwrites, }; const sagaMiddleware = createSagaMiddleware(); diff --git a/x-pack/plugins/graph/public/state_management/persistence.test.ts b/x-pack/plugins/graph/public/state_management/persistence.test.ts index b0932c92c2d1e..dc59869fafd4c 100644 --- a/x-pack/plugins/graph/public/state_management/persistence.test.ts +++ b/x-pack/plugins/graph/public/state_management/persistence.test.ts @@ -6,8 +6,14 @@ */ import { createMockGraphStore, MockedGraphEnvironment } from './mocks'; -import { loadSavedWorkspace, loadingSaga, saveWorkspace, savingSaga } from './persistence'; -import { GraphWorkspaceSavedObject, UrlTemplate, AdvancedSettings, WorkspaceField } from '../types'; +import { + loadSavedWorkspace, + loadingSaga, + saveWorkspace, + savingSaga, + LoadSavedWorkspacePayload, +} from './persistence'; +import { UrlTemplate, AdvancedSettings, WorkspaceField, GraphWorkspaceSavedObject } from '../types'; import { IndexpatternDatasource, datasourceSelector } from './datasource'; import { fieldsSelector } from './fields'; import { metaDataSelector, updateMetaData } from './meta_data'; @@ -55,7 +61,9 @@ describe('persistence sagas', () => { }); it('should deserialize saved object and populate state', async () => { env.store.dispatch( - loadSavedWorkspace({ title: 'my workspace' } as GraphWorkspaceSavedObject) + loadSavedWorkspace({ + savedWorkspace: { title: 'my workspace' }, + } as LoadSavedWorkspacePayload) ); await waitForPromise(); const resultingState = env.store.getState(); @@ -70,7 +78,7 @@ describe('persistence sagas', () => { it('should warn with a toast and abort if index pattern is not found', async () => { (migrateLegacyIndexPatternRef as jest.Mock).mockReturnValueOnce({ success: false }); - env.store.dispatch(loadSavedWorkspace({} as GraphWorkspaceSavedObject)); + env.store.dispatch(loadSavedWorkspace({ savedWorkspace: {} } as LoadSavedWorkspacePayload)); await waitForPromise(); expect(env.mockedDeps.notifications.toasts.addDanger).toHaveBeenCalled(); const resultingState = env.store.getState(); @@ -96,11 +104,10 @@ describe('persistence sagas', () => { savePolicy: 'configAndDataWithConsent', }, }); - env.mockedDeps.getSavedWorkspace().id = '123'; }); it('should serialize saved object and save after confirmation', async () => { - env.store.dispatch(saveWorkspace()); + env.store.dispatch(saveWorkspace({ id: '123' } as GraphWorkspaceSavedObject)); (openSaveModal as jest.Mock).mock.calls[0][0].saveWorkspace({}, true); expect(appStateToSavedWorkspace).toHaveBeenCalled(); await waitForPromise(); @@ -112,7 +119,7 @@ describe('persistence sagas', () => { }); it('should not save data if user does not give consent in the modal', async () => { - env.store.dispatch(saveWorkspace()); + env.store.dispatch(saveWorkspace({} as GraphWorkspaceSavedObject)); (openSaveModal as jest.Mock).mock.calls[0][0].saveWorkspace({}, false); // serialize function is called with `canSaveData` set to false expect(appStateToSavedWorkspace).toHaveBeenCalledWith( @@ -123,9 +130,8 @@ describe('persistence sagas', () => { }); it('should not change url if it was just updating existing workspace', async () => { - env.mockedDeps.getSavedWorkspace().id = '123'; env.store.dispatch(updateMetaData({ savedObjectId: '123' })); - env.store.dispatch(saveWorkspace()); + env.store.dispatch(saveWorkspace({} as GraphWorkspaceSavedObject)); await waitForPromise(); expect(env.mockedDeps.changeUrl).not.toHaveBeenCalled(); }); diff --git a/x-pack/plugins/graph/public/state_management/persistence.ts b/x-pack/plugins/graph/public/state_management/persistence.ts index f815474fa6e51..6a99eaddb32e3 100644 --- a/x-pack/plugins/graph/public/state_management/persistence.ts +++ b/x-pack/plugins/graph/public/state_management/persistence.ts @@ -8,8 +8,8 @@ import actionCreatorFactory, { Action } from 'typescript-fsa'; import { i18n } from '@kbn/i18n'; import { takeLatest, call, put, select, cps } from 'redux-saga/effects'; -import { GraphWorkspaceSavedObject, Workspace } from '../types'; -import { GraphStoreDependencies, GraphState } from '.'; +import { GraphWorkspaceSavedObject, IndexPatternSavedObject, Workspace } from '../types'; +import { GraphStoreDependencies, GraphState, submitSearch } from '.'; import { datasourceSelector } from './datasource'; import { setDatasource, IndexpatternDatasource } from './datasource'; import { loadFields, selectedFieldsSelector } from './fields'; @@ -26,10 +26,17 @@ import { openSaveModal, SaveWorkspaceHandler } from '../services/save_modal'; import { getEditPath } from '../services/url'; import { saveSavedWorkspace } from '../helpers/saved_workspace_utils'; +export interface LoadSavedWorkspacePayload { + indexPatterns: IndexPatternSavedObject[]; + savedWorkspace: GraphWorkspaceSavedObject; + urlQuery: string | null; +} + const actionCreator = actionCreatorFactory('x-pack/graph'); -export const loadSavedWorkspace = actionCreator('LOAD_WORKSPACE'); -export const saveWorkspace = actionCreator('SAVE_WORKSPACE'); +export const loadSavedWorkspace = actionCreator('LOAD_WORKSPACE'); +export const saveWorkspace = actionCreator('SAVE_WORKSPACE'); +export const fillWorkspace = actionCreator('FILL_WORKSPACE'); /** * Saga handling loading of a saved workspace. @@ -39,14 +46,12 @@ export const saveWorkspace = actionCreator('SAVE_WORKSPACE'); */ export const loadingSaga = ({ createWorkspace, - getWorkspace, - indexPatterns, notifications, indexPatternProvider, }: GraphStoreDependencies) => { - function* deserializeWorkspace(action: Action) { - const workspacePayload = action.payload; - const migrationStatus = migrateLegacyIndexPatternRef(workspacePayload, indexPatterns); + function* deserializeWorkspace(action: Action) { + const { indexPatterns, savedWorkspace, urlQuery } = action.payload; + const migrationStatus = migrateLegacyIndexPatternRef(savedWorkspace, indexPatterns); if (!migrationStatus.success) { notifications.toasts.addDanger( i18n.translate('xpack.graph.loadWorkspace.missingIndexPatternErrorMessage', { @@ -59,25 +64,24 @@ export const loadingSaga = ({ return; } - const selectedIndexPatternId = lookupIndexPatternId(workspacePayload); + const selectedIndexPatternId = lookupIndexPatternId(savedWorkspace); const indexPattern = yield call(indexPatternProvider.get, selectedIndexPatternId); const initialSettings = settingsSelector(yield select()); - createWorkspace(indexPattern.title, initialSettings); + const createdWorkspace = createWorkspace(indexPattern.title, initialSettings); const { urlTemplates, advancedSettings, allFields } = savedWorkspaceToAppState( - workspacePayload, + savedWorkspace, indexPattern, - // workspace won't be null because it's created in the same call stack - getWorkspace()! + createdWorkspace ); // put everything in the store yield put( updateMetaData({ - title: workspacePayload.title, - description: workspacePayload.description, - savedObjectId: workspacePayload.id, + title: savedWorkspace.title, + description: savedWorkspace.description, + savedObjectId: savedWorkspace.id, }) ); yield put( @@ -91,7 +95,11 @@ export const loadingSaga = ({ yield put(updateSettings(advancedSettings)); yield put(loadTemplates(urlTemplates)); - getWorkspace()!.runLayout(); + if (urlQuery) { + yield put(submitSearch(urlQuery)); + } + + createdWorkspace.runLayout(); } return function* () { @@ -105,8 +113,8 @@ export const loadingSaga = ({ * It will serialize everything and save it using the saved objects client */ export const savingSaga = (deps: GraphStoreDependencies) => { - function* persistWorkspace() { - const savedWorkspace = deps.getSavedWorkspace(); + function* persistWorkspace(action: Action) { + const savedWorkspace = action.payload; const state: GraphState = yield select(); const workspace = deps.getWorkspace(); const selectedDatasource = datasourceSelector(state).current; diff --git a/x-pack/plugins/graph/public/state_management/store.ts b/x-pack/plugins/graph/public/state_management/store.ts index 400736f7534b6..ba9bff98b0ca9 100644 --- a/x-pack/plugins/graph/public/state_management/store.ts +++ b/x-pack/plugins/graph/public/state_management/store.ts @@ -9,6 +9,7 @@ import createSagaMiddleware, { SagaMiddleware } from 'redux-saga'; import { combineReducers, createStore, Store, AnyAction, Dispatch, applyMiddleware } from 'redux'; import { ChromeStart, I18nStart, OverlayStart, SavedObjectsClientContract } from 'kibana/public'; import { CoreStart } from 'src/core/public'; +import { ReactElement } from 'react'; import { fieldsReducer, FieldsState, @@ -24,19 +25,10 @@ import { } from './advanced_settings'; import { DatasourceState, datasourceReducer } from './datasource'; import { datasourceSaga } from './datasource.sagas'; -import { - IndexPatternProvider, - Workspace, - IndexPatternSavedObject, - GraphSavePolicy, - GraphWorkspaceSavedObject, - AdvancedSettings, - WorkspaceField, - UrlTemplate, -} from '../types'; +import { IndexPatternProvider, Workspace, GraphSavePolicy, AdvancedSettings } from '../types'; import { loadingSaga, savingSaga } from './persistence'; import { metaDataReducer, MetaDataState, syncBreadcrumbSaga } from './meta_data'; -import { fillWorkspaceSaga } from './workspace'; +import { fillWorkspaceSaga, submitSearchSaga, workspaceReducer, WorkspaceState } from './workspace'; export interface GraphState { fields: FieldsState; @@ -44,28 +36,26 @@ export interface GraphState { advancedSettings: AdvancedSettingsState; datasource: DatasourceState; metaData: MetaDataState; + workspace: WorkspaceState; } export interface GraphStoreDependencies { addBasePath: (url: string) => string; indexPatternProvider: IndexPatternProvider; - indexPatterns: IndexPatternSavedObject[]; - createWorkspace: (index: string, advancedSettings: AdvancedSettings) => void; - getWorkspace: () => Workspace | null; - getSavedWorkspace: () => GraphWorkspaceSavedObject; + createWorkspace: (index: string, advancedSettings: AdvancedSettings) => Workspace; + getWorkspace: () => Workspace | undefined; notifications: CoreStart['notifications']; http: CoreStart['http']; overlays: OverlayStart; savedObjectsClient: SavedObjectsClientContract; - showSaveModal: (el: React.ReactNode, I18nContext: I18nStart['Context']) => void; + showSaveModal: (el: ReactElement, I18nContext: I18nStart['Context']) => void; savePolicy: GraphSavePolicy; changeUrl: (newUrl: string) => void; - notifyAngular: () => void; - setLiveResponseFields: (fields: WorkspaceField[]) => void; - setUrlTemplates: (templates: UrlTemplate[]) => void; - setWorkspaceInitialized: () => void; + notifyReact: () => void; chrome: ChromeStart; I18nContext: I18nStart['Context']; + basePath: string; + handleSearchQueryError: (err: Error | string) => void; } export function createRootReducer(addBasePath: (url: string) => string) { @@ -75,6 +65,7 @@ export function createRootReducer(addBasePath: (url: string) => string) { advancedSettings: advancedSettingsReducer, datasource: datasourceReducer, metaData: metaDataReducer, + workspace: workspaceReducer, }); } @@ -89,6 +80,7 @@ function registerSagas(sagaMiddleware: SagaMiddleware, deps: GraphStoreD sagaMiddleware.run(syncBreadcrumbSaga(deps)); sagaMiddleware.run(syncTemplatesSaga(deps)); sagaMiddleware.run(fillWorkspaceSaga(deps)); + sagaMiddleware.run(submitSearchSaga(deps)); } export const createGraphStore = (deps: GraphStoreDependencies) => { diff --git a/x-pack/plugins/graph/public/state_management/url_templates.ts b/x-pack/plugins/graph/public/state_management/url_templates.ts index e8f5308534e28..01b1a9296b0b6 100644 --- a/x-pack/plugins/graph/public/state_management/url_templates.ts +++ b/x-pack/plugins/graph/public/state_management/url_templates.ts @@ -10,7 +10,7 @@ import { reducerWithInitialState } from 'typescript-fsa-reducers/dist'; import { i18n } from '@kbn/i18n'; import { modifyUrl } from '@kbn/std'; import rison from 'rison-node'; -import { takeEvery, select } from 'redux-saga/effects'; +import { takeEvery } from 'redux-saga/effects'; import { format, parse } from 'url'; import { GraphState, GraphStoreDependencies } from './store'; import { UrlTemplate } from '../types'; @@ -102,11 +102,9 @@ export const templatesSelector = (state: GraphState) => state.urlTemplates; * * Won't be necessary once the side bar is moved to redux */ -export const syncTemplatesSaga = ({ setUrlTemplates, notifyAngular }: GraphStoreDependencies) => { +export const syncTemplatesSaga = ({ notifyReact }: GraphStoreDependencies) => { function* syncTemplates() { - const templates = templatesSelector(yield select()); - setUrlTemplates(templates); - notifyAngular(); + notifyReact(); } return function* () { diff --git a/x-pack/plugins/graph/public/state_management/workspace.ts b/x-pack/plugins/graph/public/state_management/workspace.ts index 4e0e481a05c17..9e8cca488e4ef 100644 --- a/x-pack/plugins/graph/public/state_management/workspace.ts +++ b/x-pack/plugins/graph/public/state_management/workspace.ts @@ -5,16 +5,41 @@ * 2.0. */ -import actionCreatorFactory from 'typescript-fsa'; +import actionCreatorFactory, { Action } from 'typescript-fsa'; import { i18n } from '@kbn/i18n'; -import { takeLatest, select, call } from 'redux-saga/effects'; -import { GraphStoreDependencies, GraphState } from '.'; +import { takeLatest, select, call, put } from 'redux-saga/effects'; +import { reducerWithInitialState } from 'typescript-fsa-reducers'; +import { createSelector } from 'reselect'; +import { GraphStoreDependencies, GraphState, fillWorkspace } from '.'; +import { reset } from './global'; import { datasourceSelector } from './datasource'; -import { selectedFieldsSelector } from './fields'; +import { liveResponseFieldsSelector, selectedFieldsSelector } from './fields'; import { fetchTopNodes } from '../services/fetch_top_nodes'; -const actionCreator = actionCreatorFactory('x-pack/graph'); +import { Workspace } from '../types'; -export const fillWorkspace = actionCreator('FILL_WORKSPACE'); +const actionCreator = actionCreatorFactory('x-pack/graph/workspace'); + +export interface WorkspaceState { + isInitialized: boolean; +} + +const initialWorkspaceState: WorkspaceState = { + isInitialized: false, +}; + +export const initializeWorkspace = actionCreator('INITIALIZE_WORKSPACE'); +export const submitSearch = actionCreator('SUBMIT_SEARCH'); + +export const workspaceReducer = reducerWithInitialState(initialWorkspaceState) + .case(reset, () => ({ isInitialized: false })) + .case(initializeWorkspace, () => ({ isInitialized: true })) + .build(); + +export const workspaceSelector = (state: GraphState) => state.workspace; +export const workspaceInitializedSelector = createSelector( + workspaceSelector, + (workspace: WorkspaceState) => workspace.isInitialized +); /** * Saga handling filling in top terms into workspace. @@ -23,8 +48,7 @@ export const fillWorkspace = actionCreator('FILL_WORKSPACE'); */ export const fillWorkspaceSaga = ({ getWorkspace, - setWorkspaceInitialized, - notifyAngular, + notifyReact, http, notifications, }: GraphStoreDependencies) => { @@ -47,8 +71,8 @@ export const fillWorkspaceSaga = ({ nodes: topTermNodes, edges: [], }); - setWorkspaceInitialized(); - notifyAngular(); + yield put(initializeWorkspace()); + notifyReact(); workspace.fillInGraph(fields.length * 10); } catch (e) { const message = 'body' in e ? e.body.message : e.message; @@ -65,3 +89,39 @@ export const fillWorkspaceSaga = ({ yield takeLatest(fillWorkspace.match, fetchNodes); }; }; + +export const submitSearchSaga = ({ + getWorkspace, + handleSearchQueryError, +}: GraphStoreDependencies) => { + function* submit(action: Action) { + const searchTerm = action.payload; + yield put(initializeWorkspace()); + + // type casting is safe, at this point workspace should be loaded + const workspace = getWorkspace() as Workspace; + const numHops = 2; + const liveResponseFields = liveResponseFieldsSelector(yield select()); + + if (searchTerm.startsWith('{')) { + try { + const query = JSON.parse(searchTerm); + if (query.vertices) { + // Is a graph explore request + workspace.callElasticsearch(query); + } else { + // Is a regular query DSL query + workspace.search(query, liveResponseFields, numHops); + } + } catch (err) { + handleSearchQueryError(err); + } + return; + } + workspace.simpleSearch(searchTerm, liveResponseFields, numHops); + } + + return function* () { + yield takeLatest(submitSearch.match, submit); + }; +}; diff --git a/x-pack/plugins/graph/public/types/persistence.ts b/x-pack/plugins/graph/public/types/persistence.ts index 46d711de04205..640348d96f6ac 100644 --- a/x-pack/plugins/graph/public/types/persistence.ts +++ b/x-pack/plugins/graph/public/types/persistence.ts @@ -53,15 +53,15 @@ export interface SerializedField extends Omit { +export interface SerializedNode extends Pick { field: string; term: string; parent: number | null; size: number; } -export interface SerializedEdge extends Omit { +export interface SerializedEdge + extends Omit { source: number; target: number; } diff --git a/x-pack/plugins/graph/public/types/workspace_state.ts b/x-pack/plugins/graph/public/types/workspace_state.ts index 86f05376b9526..bca94a7cfad6d 100644 --- a/x-pack/plugins/graph/public/types/workspace_state.ts +++ b/x-pack/plugins/graph/public/types/workspace_state.ts @@ -6,10 +6,13 @@ */ import { JsonObject } from '@kbn/utility-types'; +import d3 from 'd3'; +import { TargetOptions } from '../components/control_panel'; import { FontawesomeIcon } from '../helpers/style_choices'; import { WorkspaceField, AdvancedSettings } from './app_state'; export interface WorkspaceNode { + id: string; x: number; y: number; label: string; @@ -21,9 +24,14 @@ export interface WorkspaceNode { scaledSize: number; parent: WorkspaceNode | null; color: string; + numChildren: number; isSelected?: boolean; + kx: number; + ky: number; } +export type BlockListedNode = Omit; + export interface WorkspaceEdge { weight: number; width: number; @@ -31,6 +39,8 @@ export interface WorkspaceEdge { source: WorkspaceNode; target: WorkspaceNode; isSelected?: boolean; + topTarget: WorkspaceNode; + topSrc: WorkspaceNode; } export interface ServerResultNode { @@ -58,13 +68,59 @@ export interface GraphData { nodes: ServerResultNode[]; edges: ServerResultEdge[]; } +export interface TermIntersect { + id1: string; + id2: string; + term1: string; + term2: string; + v1: number; + v2: number; + overlap: number; +} export interface Workspace { options: WorkspaceOptions; nodesMap: Record; nodes: WorkspaceNode[]; + selectedNodes: WorkspaceNode[]; edges: WorkspaceEdge[]; - blocklistedNodes: WorkspaceNode[]; + blocklistedNodes: BlockListedNode[]; + undoLog: string; + redoLog: string; + force: ReturnType; + lastRequest: string; + lastResponse: string; + + undo: () => void; + redo: () => void; + expandSelecteds: (targetOptions: TargetOptions) => {}; + deleteSelection: () => void; + blocklistSelection: () => void; + selectAll: () => void; + selectNone: () => void; + selectInvert: () => void; + selectNeighbours: () => void; + deselectNode: (node: WorkspaceNode) => void; + colorSelected: (color: string) => void; + groupSelections: (node: WorkspaceNode | undefined) => void; + ungroup: (node: WorkspaceNode | undefined) => void; + callElasticsearch: (request: any) => void; + search: (qeury: any, fieldsChoice: WorkspaceField[] | undefined, numHops: number) => void; + simpleSearch: ( + searchTerm: string, + fieldsChoice: WorkspaceField[] | undefined, + numHops: number + ) => void; + getAllIntersections: ( + callback: (termIntersects: TermIntersect[]) => void, + nodes: WorkspaceNode[] + ) => void; + toggleNodeSelection: (node: WorkspaceNode) => boolean; + mergeIds: (term1: string, term2: string) => void; + changeHandler: () => void; + unblockNode: (node: BlockListedNode) => void; + unblockAll: () => void; + clearGraph: () => void; getQuery(startNodes?: WorkspaceNode[], loose?: boolean): JsonObject; getSelectedOrAllNodes(): WorkspaceNode[]; @@ -96,6 +152,8 @@ export type ExploreRequest = any; export type SearchRequest = any; export type ExploreResults = any; export type SearchResults = any; +export type GraphExploreCallback = (data: ExploreResults) => void; +export type GraphSearchCallback = (data: SearchResults) => void; export type WorkspaceOptions = Partial<{ indexName: string; @@ -105,12 +163,14 @@ export type WorkspaceOptions = Partial<{ graphExploreProxy: ( indexPattern: string, request: ExploreRequest, - callback: (data: ExploreResults) => void + callback: GraphExploreCallback ) => void; searchProxy: ( indexPattern: string, request: SearchRequest, - callback: (data: SearchResults) => void + callback: GraphSearchCallback ) => void; exploreControls: AdvancedSettings; }>; + +export type ControlType = 'style' | 'drillDowns' | 'editLabel' | 'mergeTerms' | 'none'; From 95eab7ccca0c66c0291ee248c1f900e5970d8be8 Mon Sep 17 00:00:00 2001 From: Dima Arnautov Date: Tue, 31 Aug 2021 19:54:13 +0200 Subject: [PATCH 04/81] [ML] Fix "Show charts" control state (#110602) * [ML] fix show charts state * [ML] fix export --- .../checkbox_showcharts.tsx | 34 ++++++------------- .../controls/checkbox_showcharts/index.ts | 2 +- .../public/application/explorer/explorer.js | 5 ++- .../explorer/explorer_constants.ts | 1 + .../explorer/explorer_dashboard_service.ts | 7 ++++ .../reducers/explorer_reducer/reducer.ts | 7 ++++ .../reducers/explorer_reducer/state.ts | 2 ++ .../application/routing/routes/explorer.tsx | 14 +++++--- 8 files changed, 43 insertions(+), 29 deletions(-) diff --git a/x-pack/plugins/ml/public/application/components/controls/checkbox_showcharts/checkbox_showcharts.tsx b/x-pack/plugins/ml/public/application/components/controls/checkbox_showcharts/checkbox_showcharts.tsx index 2fa81504f93cb..73eb91ffd30a8 100644 --- a/x-pack/plugins/ml/public/application/components/controls/checkbox_showcharts/checkbox_showcharts.tsx +++ b/x-pack/plugins/ml/public/application/components/controls/checkbox_showcharts/checkbox_showcharts.tsx @@ -8,34 +8,22 @@ import React, { FC, useCallback, useMemo } from 'react'; import { EuiCheckbox, htmlIdGenerator } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { useExplorerUrlState } from '../../../explorer/hooks/use_explorer_url_state'; -const SHOW_CHARTS_DEFAULT = true; - -export const useShowCharts = (): [boolean, (v: boolean) => void] => { - const [explorerUrlState, setExplorerUrlState] = useExplorerUrlState(); - - const showCharts = explorerUrlState?.mlShowCharts ?? SHOW_CHARTS_DEFAULT; - - const setShowCharts = useCallback( - (v: boolean) => { - setExplorerUrlState({ mlShowCharts: v }); - }, - [setExplorerUrlState] - ); - - return [showCharts, setShowCharts]; -}; +export interface CheckboxShowChartsProps { + showCharts: boolean; + setShowCharts: (update: boolean) => void; +} /* * React component for a checkbox element to toggle charts display. */ -export const CheckboxShowCharts: FC = () => { - const [showCharts, setShowCharts] = useShowCharts(); - - const onChange = (e: React.ChangeEvent) => { - setShowCharts(e.target.checked); - }; +export const CheckboxShowCharts: FC = ({ showCharts, setShowCharts }) => { + const onChange = useCallback( + (e: React.ChangeEvent) => { + setShowCharts(e.target.checked); + }, + [setShowCharts] + ); const id = useMemo(() => htmlIdGenerator()(), []); diff --git a/x-pack/plugins/ml/public/application/components/controls/checkbox_showcharts/index.ts b/x-pack/plugins/ml/public/application/components/controls/checkbox_showcharts/index.ts index 3ff95bf6e335c..2099abb168283 100644 --- a/x-pack/plugins/ml/public/application/components/controls/checkbox_showcharts/index.ts +++ b/x-pack/plugins/ml/public/application/components/controls/checkbox_showcharts/index.ts @@ -5,4 +5,4 @@ * 2.0. */ -export { useShowCharts, CheckboxShowCharts } from './checkbox_showcharts'; +export { CheckboxShowCharts } from './checkbox_showcharts'; diff --git a/x-pack/plugins/ml/public/application/explorer/explorer.js b/x-pack/plugins/ml/public/application/explorer/explorer.js index c9365c4edbe5f..daecf7585b3ea 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer.js +++ b/x-pack/plugins/ml/public/application/explorer/explorer.js @@ -498,7 +498,10 @@ export class ExplorerUI extends React.Component { {chartsData.seriesToPlot.length > 0 && selectedCells !== undefined && ( - + )} diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_constants.ts b/x-pack/plugins/ml/public/application/explorer/explorer_constants.ts index d737c4733b9cb..cd01de31e5e60 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer_constants.ts +++ b/x-pack/plugins/ml/public/application/explorer/explorer_constants.ts @@ -34,6 +34,7 @@ export const EXPLORER_ACTION = { SET_VIEW_BY_PER_PAGE: 'setViewByPerPage', SET_VIEW_BY_FROM_PAGE: 'setViewByFromPage', SET_SWIM_LANE_SEVERITY: 'setSwimLaneSeverity', + SET_SHOW_CHARTS: 'setShowCharts', }; export const FILTER_ACTION = { diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_dashboard_service.ts b/x-pack/plugins/ml/public/application/explorer/explorer_dashboard_service.ts index f858c40b32315..1d4a277af0131 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer_dashboard_service.ts +++ b/x-pack/plugins/ml/public/application/explorer/explorer_dashboard_service.ts @@ -83,6 +83,10 @@ const explorerAppState$: Observable = explorerState$.pipe( appState.mlExplorerSwimlane.severity = state.swimLaneSeverity; } + if (state.showCharts !== undefined) { + appState.mlShowCharts = state.showCharts; + } + if (state.filterActive) { appState.mlExplorerFilter.influencersFilterQuery = state.influencersFilterQuery; appState.mlExplorerFilter.filterActive = state.filterActive; @@ -168,6 +172,9 @@ export const explorerService = { setSwimLaneSeverity: (payload: number) => { explorerAction$.next({ type: EXPLORER_ACTION.SET_SWIM_LANE_SEVERITY, payload }); }, + setShowCharts: (payload: boolean) => { + explorerAction$.next({ type: EXPLORER_ACTION.SET_SHOW_CHARTS, payload }); + }, }; export type ExplorerService = typeof explorerService; diff --git a/x-pack/plugins/ml/public/application/explorer/reducers/explorer_reducer/reducer.ts b/x-pack/plugins/ml/public/application/explorer/reducers/explorer_reducer/reducer.ts index 74867af5f8987..192699afc2cf4 100644 --- a/x-pack/plugins/ml/public/application/explorer/reducers/explorer_reducer/reducer.ts +++ b/x-pack/plugins/ml/public/application/explorer/reducers/explorer_reducer/reducer.ts @@ -158,6 +158,13 @@ export const explorerReducer = (state: ExplorerState, nextAction: Action): Explo }; break; + case EXPLORER_ACTION.SET_SHOW_CHARTS: + nextState = { + ...state, + showCharts: payload, + }; + break; + default: nextState = state; } diff --git a/x-pack/plugins/ml/public/application/explorer/reducers/explorer_reducer/state.ts b/x-pack/plugins/ml/public/application/explorer/reducers/explorer_reducer/state.ts index a06db20210c1b..202a4389ef524 100644 --- a/x-pack/plugins/ml/public/application/explorer/reducers/explorer_reducer/state.ts +++ b/x-pack/plugins/ml/public/application/explorer/reducers/explorer_reducer/state.ts @@ -59,6 +59,7 @@ export interface ExplorerState { viewBySwimlaneOptions: string[]; swimlaneLimit?: number; swimLaneSeverity?: number; + showCharts: boolean; } function getDefaultIndexPattern() { @@ -112,5 +113,6 @@ export function getExplorerDefaultState(): ExplorerState { viewByPerPage: SWIM_LANE_DEFAULT_PAGE_SIZE, viewByFromPage: 1, swimlaneLimit: undefined, + showCharts: true, }; } diff --git a/x-pack/plugins/ml/public/application/routing/routes/explorer.tsx b/x-pack/plugins/ml/public/application/routing/routes/explorer.tsx index 42927d9b4ef50..49e7857eee082 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/explorer.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/explorer.tsx @@ -26,7 +26,6 @@ import { useExplorerData } from '../../explorer/actions'; import { explorerService } from '../../explorer/explorer_dashboard_service'; import { getDateFormatTz } from '../../explorer/explorer_utils'; import { useJobSelection } from '../../components/job_selector/use_job_selection'; -import { useShowCharts } from '../../components/controls/checkbox_showcharts'; import { useTableInterval } from '../../components/controls/select_interval'; import { useTableSeverity } from '../../components/controls/select_severity'; import { useUrlState } from '../../util/url_state'; @@ -196,6 +195,10 @@ const ExplorerUrlStateManager: FC = ({ jobsWithTim if (severity !== undefined) { explorerService.setSwimLaneSeverity(severity); } + + if (explorerUrlState.mlShowCharts !== undefined) { + explorerService.setShowCharts(explorerUrlState.mlShowCharts); + } }, []); /** Sync URL state with {@link explorerService} state */ @@ -214,7 +217,6 @@ const ExplorerUrlStateManager: FC = ({ jobsWithTim } }, [explorerData]); - const [showCharts] = useShowCharts(); const [tableInterval] = useTableInterval(); const [tableSeverity] = useTableSeverity(); @@ -267,7 +269,11 @@ const ExplorerUrlStateManager: FC = ({ jobsWithTim } }, [JSON.stringify(loadExplorerDataConfig), selectedCells?.showTopFieldValues]); - if (explorerState === undefined || refresh === undefined || showCharts === undefined) { + if ( + explorerState === undefined || + refresh === undefined || + explorerAppState?.mlShowCharts === undefined + ) { return null; } @@ -277,7 +283,7 @@ const ExplorerUrlStateManager: FC = ({ jobsWithTim {...{ explorerState, setSelectedCells, - showCharts, + showCharts: explorerState.showCharts, severity: tableSeverity.val, stoppedPartitions, invalidTimeRangeError, From a3fd138da1aefcd2c66465f348ec6af0acd8f2c7 Mon Sep 17 00:00:00 2001 From: Mikhail Shustov Date: Tue, 31 Aug 2021 21:06:47 +0300 Subject: [PATCH 05/81] do not make an assumption on user-supplied data content (#109425) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- src/core/server/elasticsearch/client/client_config.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/core/server/elasticsearch/client/client_config.ts b/src/core/server/elasticsearch/client/client_config.ts index 27d6f877a5572..a6b0891fc12dd 100644 --- a/src/core/server/elasticsearch/client/client_config.ts +++ b/src/core/server/elasticsearch/client/client_config.ts @@ -56,6 +56,9 @@ export function parseClientOptions( ...DEFAULT_HEADERS, ...config.customHeaders, }, + // do not make assumption on user-supplied data content + // fixes https://github.com/elastic/kibana/issues/101944 + disablePrototypePoisoningProtection: true, }; if (config.pingTimeout != null) { From f8c80a74222b9a57e52cf3b7d46d74311f00508e Mon Sep 17 00:00:00 2001 From: Gloria Hornero Date: Tue, 31 Aug 2021 20:07:06 +0200 Subject: [PATCH 06/81] [Security Solution] Updates loock-back time on Cypress tests (#110609) * updates loock-back time * updates loock-back value for 'expectedExportedRule' --- x-pack/plugins/security_solution/cypress/objects/rule.ts | 4 ++-- .../security_solution/cypress/tasks/api_calls/rules.ts | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/x-pack/plugins/security_solution/cypress/objects/rule.ts b/x-pack/plugins/security_solution/cypress/objects/rule.ts index 41027258f0bf0..c3eab5cc2a936 100644 --- a/x-pack/plugins/security_solution/cypress/objects/rule.ts +++ b/x-pack/plugins/security_solution/cypress/objects/rule.ts @@ -164,7 +164,7 @@ const getRunsEvery = (): Interval => ({ }); const getLookBack = (): Interval => ({ - interval: '17520', + interval: '50000', timeType: 'Hours', type: 'h', }); @@ -382,5 +382,5 @@ export const getEditedRule = (): CustomRule => ({ export const expectedExportedRule = (ruleResponse: Cypress.Response): string => { const jsonrule = ruleResponse.body; - return `{"id":"${jsonrule.id}","updated_at":"${jsonrule.updated_at}","updated_by":"elastic","created_at":"${jsonrule.created_at}","created_by":"elastic","name":"${jsonrule.name}","tags":[],"interval":"100m","enabled":false,"description":"${jsonrule.description}","risk_score":${jsonrule.risk_score},"severity":"${jsonrule.severity}","output_index":".siem-signals-default","author":[],"false_positives":[],"from":"now-17520h","rule_id":"rule_testing","max_signals":100,"risk_score_mapping":[],"severity_mapping":[],"threat":[],"to":"now","references":[],"version":1,"exceptions_list":[],"immutable":false,"type":"query","language":"kuery","index":["exceptions-*"],"query":"${jsonrule.query}","throttle":"no_actions","actions":[]}\n{"exported_count":1,"missing_rules":[],"missing_rules_count":0}\n`; + return `{"id":"${jsonrule.id}","updated_at":"${jsonrule.updated_at}","updated_by":"elastic","created_at":"${jsonrule.created_at}","created_by":"elastic","name":"${jsonrule.name}","tags":[],"interval":"100m","enabled":false,"description":"${jsonrule.description}","risk_score":${jsonrule.risk_score},"severity":"${jsonrule.severity}","output_index":".siem-signals-default","author":[],"false_positives":[],"from":"now-50000h","rule_id":"rule_testing","max_signals":100,"risk_score_mapping":[],"severity_mapping":[],"threat":[],"to":"now","references":[],"version":1,"exceptions_list":[],"immutable":false,"type":"query","language":"kuery","index":["exceptions-*"],"query":"${jsonrule.query}","throttle":"no_actions","actions":[]}\n{"exported_count":1,"missing_rules":[],"missing_rules_count":0}\n`; }; diff --git a/x-pack/plugins/security_solution/cypress/tasks/api_calls/rules.ts b/x-pack/plugins/security_solution/cypress/tasks/api_calls/rules.ts index b4e4941ff7f94..33bd8a06b9985 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/api_calls/rules.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/api_calls/rules.ts @@ -19,7 +19,7 @@ export const createCustomRule = (rule: CustomRule, ruleId = 'rule_testing', inte name: rule.name, severity: rule.severity.toLocaleLowerCase(), type: 'query', - from: 'now-17520h', + from: 'now-50000h', index: ['exceptions-*'], query: rule.customQuery, language: 'kuery', @@ -59,7 +59,7 @@ export const createCustomIndicatorRule = (rule: ThreatIndicatorRule, ruleId = 'r threat_filters: [], threat_index: rule.indicatorIndexPattern, threat_indicator_path: '', - from: 'now-17520h', + from: 'now-50000h', index: rule.index, query: rule.customQuery || '*:*', language: 'kuery', @@ -86,7 +86,7 @@ export const createCustomRuleActivated = ( name: rule.name, severity: rule.severity.toLocaleLowerCase(), type: 'query', - from: 'now-17520h', + from: 'now-50000h', index: rule.index, query: rule.customQuery, language: 'kuery', From bbfad1905124ba21d5dcfcddacab042cd66183cc Mon Sep 17 00:00:00 2001 From: Tim Sullivan Date: Tue, 31 Aug 2021 11:07:56 -0700 Subject: [PATCH 07/81] [Reporting] Remove `any` from public/poller (#110539) * [Reporting] Remove `any` from public/poller * remove unnecessary comment --- x-pack/plugins/reporting/common/poller.ts | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/x-pack/plugins/reporting/common/poller.ts b/x-pack/plugins/reporting/common/poller.ts index 13ded0576bdf5..3778454c3a4cc 100644 --- a/x-pack/plugins/reporting/common/poller.ts +++ b/x-pack/plugins/reporting/common/poller.ts @@ -8,20 +8,19 @@ import _ from 'lodash'; interface PollerOptions { - functionToPoll: () => Promise; + functionToPoll: () => Promise; pollFrequencyInMillis: number; trailing?: boolean; continuePollingOnError?: boolean; pollFrequencyErrorMultiplier?: number; - successFunction?: (...args: any) => any; - errorFunction?: (error: Error) => any; + successFunction?: (...args: unknown[]) => void; + errorFunction?: (error: Error) => void; } -// @TODO Maybe move to observables someday export class Poller { - private readonly functionToPoll: () => Promise; - private readonly successFunction: (...args: any) => any; - private readonly errorFunction: (error: Error) => any; + private readonly functionToPoll: () => Promise; + private readonly successFunction: (...args: unknown[]) => void; + private readonly errorFunction: (error: Error) => void; private _isRunning: boolean; private _timeoutId: NodeJS.Timeout | null; private pollFrequencyInMillis: number; From 42acf39a703027cde4f0d74d8651a063d0a348e2 Mon Sep 17 00:00:00 2001 From: Quynh Nguyen <43350163+qn895@users.noreply.github.com> Date: Tue, 31 Aug 2021 13:08:17 -0500 Subject: [PATCH 08/81] [ML] Populate date fields for Transform (#108804) * [ML] Add index pattern info & select control for date time * [ML] Update translations * [ML] Gracefully handle when index pattern is not available * [ML] Fix import * [ML] Handle when unmounted * [ML] Remove load index patterns because we don't really need it * [ML] Add error obj to error toasts * [ML] Update tests * [ML] Update hook Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../action_clone/use_clone_action.tsx | 12 +- .../action_edit/use_edit_action.tsx | 56 ++++++++-- .../edit_transform_flyout.tsx | 13 ++- .../edit_transform_flyout_form.tsx | 103 +++++++++++++++--- .../components/transform_list/use_actions.tsx | 6 +- .../translations/translations/ja-JP.json | 1 - .../translations/translations/zh-CN.json | 1 - .../permissions/full_transform_access.ts | 5 +- .../services/transform/edit_flyout.ts | 15 +++ 9 files changed, 172 insertions(+), 40 deletions(-) diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/action_clone/use_clone_action.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_clone/use_clone_action.tsx index 6249e77ce31dc..55576c3f3ee7d 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/action_clone/use_clone_action.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_clone/use_clone_action.tsx @@ -42,8 +42,8 @@ export const useCloneAction = (forceDisable: boolean, transformNodes: number) => toastNotifications.addDanger( i18n.translate('xpack.transform.clone.noIndexPatternErrorPromptText', { defaultMessage: - 'Unable to clone the transform . No index pattern exists for {indexPattern}.', - values: { indexPattern: indexPatternTitle }, + 'Unable to clone the transform {transformId}. No index pattern exists for {indexPattern}.', + values: { indexPattern: indexPatternTitle, transformId: item.id }, }) ); } else { @@ -52,11 +52,11 @@ export const useCloneAction = (forceDisable: boolean, transformNodes: number) => ); } } catch (e) { - toastNotifications.addDanger( - i18n.translate('xpack.transform.clone.errorPromptText', { + toastNotifications.addError(e, { + title: i18n.translate('xpack.transform.clone.errorPromptText', { defaultMessage: 'An error occurred checking if source index pattern exists', - }) - ); + }), + }); } }, [ diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/action_edit/use_edit_action.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_edit/use_edit_action.tsx index b84b309c478fd..03e45b8271952 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/action_edit/use_edit_action.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_edit/use_edit_action.tsx @@ -5,25 +5,63 @@ * 2.0. */ -import React, { useContext, useMemo, useState } from 'react'; +import React, { useCallback, useContext, useMemo, useState } from 'react'; -import { TransformConfigUnion } from '../../../../../../common/types/transform'; +import { i18n } from '@kbn/i18n'; import { TransformListAction, TransformListRow } from '../../../../common'; import { AuthorizationContext } from '../../../../lib/authorization'; import { editActionNameText, EditActionName } from './edit_action_name'; +import { useSearchItems } from '../../../../hooks/use_search_items'; +import { useAppDependencies, useToastNotifications } from '../../../../app_dependencies'; +import { TransformConfigUnion } from '../../../../../../common/types/transform'; export const useEditAction = (forceDisable: boolean, transformNodes: number) => { const { canCreateTransform } = useContext(AuthorizationContext).capabilities; const [config, setConfig] = useState(); const [isFlyoutVisible, setIsFlyoutVisible] = useState(false); + const [indexPatternId, setIndexPatternId] = useState(); + const closeFlyout = () => setIsFlyoutVisible(false); - const showFlyout = (newConfig: TransformConfigUnion) => { - setConfig(newConfig); - setIsFlyoutVisible(true); - }; + + const { getIndexPatternIdByTitle } = useSearchItems(undefined); + const toastNotifications = useToastNotifications(); + const appDeps = useAppDependencies(); + const indexPatterns = appDeps.data.indexPatterns; + + const clickHandler = useCallback( + async (item: TransformListRow) => { + try { + const indexPatternTitle = Array.isArray(item.config.source.index) + ? item.config.source.index.join(',') + : item.config.source.index; + const currentIndexPatternId = getIndexPatternIdByTitle(indexPatternTitle); + + if (currentIndexPatternId === undefined) { + toastNotifications.addWarning( + i18n.translate('xpack.transform.edit.noIndexPatternErrorPromptText', { + defaultMessage: + 'Unable to get index pattern the transform {transformId}. No index pattern exists for {indexPattern}.', + values: { indexPattern: indexPatternTitle, transformId: item.id }, + }) + ); + } + setIndexPatternId(currentIndexPatternId); + setConfig(item.config); + setIsFlyoutVisible(true); + } catch (e) { + toastNotifications.addError(e, { + title: i18n.translate('xpack.transform.edit.errorPromptText', { + defaultMessage: 'An error occurred checking if source index pattern exists', + }), + }); + } + }, + // eslint-disable-next-line react-hooks/exhaustive-deps + [indexPatterns, toastNotifications, getIndexPatternIdByTitle] + ); const action: TransformListAction = useMemo( () => ({ @@ -32,10 +70,10 @@ export const useEditAction = (forceDisable: boolean, transformNodes: number) => description: editActionNameText, icon: 'pencil', type: 'icon', - onClick: (item: TransformListRow) => showFlyout(item.config), + onClick: (item: TransformListRow) => clickHandler(item), 'data-test-subj': 'transformActionEdit', }), - [canCreateTransform, forceDisable, transformNodes] + [canCreateTransform, clickHandler, forceDisable, transformNodes] ); return { @@ -43,6 +81,6 @@ export const useEditAction = (forceDisable: boolean, transformNodes: number) => config, closeFlyout, isFlyoutVisible, - showFlyout, + indexPatternId, }; }; diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_flyout.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_flyout.tsx index faa304678c0fa..55225e0ff45c0 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_flyout.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_flyout.tsx @@ -30,7 +30,6 @@ import { getErrorMessage } from '../../../../../../common/utils/errors'; import { refreshTransformList$, REFRESH_TRANSFORM_LIST_STATE } from '../../../../common'; import { useToastNotifications } from '../../../../app_dependencies'; - import { useApi } from '../../../../hooks/use_api'; import { EditTransformFlyoutCallout } from './edit_transform_flyout_callout'; @@ -43,9 +42,14 @@ import { interface EditTransformFlyoutProps { closeFlyout: () => void; config: TransformConfigUnion; + indexPatternId?: string; } -export const EditTransformFlyout: FC = ({ closeFlyout, config }) => { +export const EditTransformFlyout: FC = ({ + closeFlyout, + config, + indexPatternId, +}) => { const api = useApi(); const toastNotifications = useToastNotifications(); @@ -96,7 +100,10 @@ export const EditTransformFlyout: FC = ({ closeFlyout, }> - + {errorMessage !== undefined && ( <> diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_flyout_form.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_flyout_form.tsx index d434e2e719f5e..40ccd68724400 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_flyout_form.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_flyout_form.tsx @@ -5,23 +5,58 @@ * 2.0. */ -import React, { FC } from 'react'; +import React, { FC, useEffect, useMemo, useState } from 'react'; -import { EuiForm, EuiAccordion, EuiSpacer } from '@elastic/eui'; +import { EuiForm, EuiAccordion, EuiSpacer, EuiSelect, EuiFormRow } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { EditTransformFlyoutFormTextInput } from './edit_transform_flyout_form_text_input'; import { UseEditTransformFlyoutReturnType } from './use_edit_transform_flyout'; +import { useAppDependencies } from '../../../../app_dependencies'; +import { KBN_FIELD_TYPES } from '../../../../../../../../../src/plugins/data/common'; interface EditTransformFlyoutFormProps { editTransformFlyout: UseEditTransformFlyoutReturnType; + indexPatternId?: string; } export const EditTransformFlyoutForm: FC = ({ editTransformFlyout: [state, dispatch], + indexPatternId, }) => { const formFields = state.formFields; + const [dateFieldNames, setDateFieldNames] = useState([]); + + const appDeps = useAppDependencies(); + const indexPatternsClient = appDeps.data.indexPatterns; + + useEffect( + function getDateFields() { + let unmounted = false; + if (indexPatternId !== undefined) { + indexPatternsClient.get(indexPatternId).then((indexPattern) => { + if (indexPattern) { + const dateTimeFields = indexPattern.fields + .filter((f) => f.type === KBN_FIELD_TYPES.DATE) + .map((f) => f.name) + .sort(); + if (!unmounted) { + setDateFieldNames(dateTimeFields); + } + } + }); + return () => { + unmounted = true; + }; + } + }, + [indexPatternId, indexPatternsClient] + ); + + const retentionDateFieldOptions = useMemo(() => { + return Array.isArray(dateFieldNames) ? dateFieldNames.map((text: string) => ({ text })) : []; + }, [dateFieldNames]); return ( @@ -112,19 +147,57 @@ export const EditTransformFlyoutForm: FC = ({ paddingSize="s" >
- {' '} - dispatch({ field: 'retentionPolicyField', value })} - value={formFields.retentionPolicyField.value} - /> + { + // If index pattern or date fields info not available + // gracefully defaults to text input + indexPatternId ? ( + 0} + error={formFields.retentionPolicyField.errorMessages} + helpText={i18n.translate( + 'xpack.transform.transformList.editFlyoutFormRetentionPolicyDateFieldHelpText', + { + defaultMessage: + 'Select the date field that can be used to identify out of date documents in the destination index.', + } + )} + > + + dispatch({ field: 'retentionPolicyField', value: e.target.value }) + } + /> + + ) : ( + dispatch({ field: 'retentionPolicyField', value })} + value={formFields.retentionPolicyField.value} + /> + ) + } {startAction.isModalVisible && } {editAction.config && editAction.isFlyoutVisible && ( - + )} {deleteAction.isModalVisible && } diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index a41e0695a1bd7..770f24114d8ef 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -23566,7 +23566,6 @@ "xpack.transform.clone.errorPromptText": "ソースインデックスパターンが存在するかどうかを確認するときにエラーが発生しました", "xpack.transform.clone.errorPromptTitle": "変換構成の取得中にエラーが発生しました。", "xpack.transform.clone.fetchErrorPromptText": "KibanaインデックスパターンIDを取得できませんでした。", - "xpack.transform.clone.noIndexPatternErrorPromptText": "変換を複製できません。{indexPattern}のインデックスパターンが存在しません。", "xpack.transform.cloneTransform.breadcrumbTitle": "クローン変換", "xpack.transform.createTransform.breadcrumbTitle": "変換の作成", "xpack.transform.deleteTransform.deleteAnalyticsWithIndexErrorMessage": "ディスティネーションインデックス{destinationIndex}の削除中にエラーが発生しました", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 8d2e3607d1d32..c28fdbed5f31c 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -24117,7 +24117,6 @@ "xpack.transform.clone.errorPromptText": "检查源索引模式是否存在时发生错误", "xpack.transform.clone.errorPromptTitle": "获取转换配置时发生错误。", "xpack.transform.clone.fetchErrorPromptText": "无法提取 Kibana 索引模式 ID。", - "xpack.transform.clone.noIndexPatternErrorPromptText": "无法克隆转换。对于 {indexPattern},不存在索引模式。", "xpack.transform.cloneTransform.breadcrumbTitle": "克隆转换", "xpack.transform.createTransform.breadcrumbTitle": "创建转换", "xpack.transform.deleteTransform.deleteAnalyticsWithIndexErrorMessage": "删除目标索引 {destinationIndex} 时发生错误", diff --git a/x-pack/test/functional/apps/transform/permissions/full_transform_access.ts b/x-pack/test/functional/apps/transform/permissions/full_transform_access.ts index d50943fad991a..5f74b2da213b0 100644 --- a/x-pack/test/functional/apps/transform/permissions/full_transform_access.ts +++ b/x-pack/test/functional/apps/transform/permissions/full_transform_access.ts @@ -158,10 +158,7 @@ export default function ({ getService }: FtrProviderContext) { 'should have the retention policy inputs enabled' ); await transform.editFlyout.openTransformEditAccordionRetentionPolicySettings(); - await transform.editFlyout.assertTransformEditFlyoutInputEnabled( - 'RetentionPolicyField', - true - ); + await transform.editFlyout.assertTransformEditFlyoutRetentionPolicySelectEnabled(true); await transform.editFlyout.assertTransformEditFlyoutInputEnabled( 'RetentionPolicyMaxAge', true diff --git a/x-pack/test/functional/services/transform/edit_flyout.ts b/x-pack/test/functional/services/transform/edit_flyout.ts index fcb87fc9bec5b..cc230e2c38fca 100644 --- a/x-pack/test/functional/services/transform/edit_flyout.ts +++ b/x-pack/test/functional/services/transform/edit_flyout.ts @@ -37,6 +37,21 @@ export function TransformEditFlyoutProvider({ getService }: FtrProviderContext) ); }, + async assertTransformEditFlyoutRetentionPolicySelectEnabled(expectedValue: boolean) { + await testSubjects.existOrFail(`transformEditFlyoutRetentionPolicyFieldSelect`, { + timeout: 1000, + }); + const isEnabled = await testSubjects.isEnabled( + `transformEditFlyoutRetentionPolicyFieldSelect` + ); + expect(isEnabled).to.eql( + expectedValue, + `Expected 'transformEditFlyoutRetentionPolicyFieldSelect' input to be '${ + expectedValue ? 'enabled' : 'disabled' + }' (got '${isEnabled ? 'enabled' : 'disabled'}')` + ); + }, + async assertTransformEditFlyoutInputEnabled(input: string, expectedValue: boolean) { await testSubjects.existOrFail(`transformEditFlyout${input}Input`, { timeout: 1000 }); const isEnabled = await testSubjects.isEnabled(`transformEditFlyout${input}Input`); From ca120eef9166be93398876f5c8af988478b13670 Mon Sep 17 00:00:00 2001 From: Tim Sullivan Date: Tue, 31 Aug 2021 11:08:49 -0700 Subject: [PATCH 09/81] [Reporting] Remove `any` from pdf job compatibility shim (#110555) * [Reporting] Remove `any` from pdf job compatibility shim * remove `any` usage in a few other isolated areas --- .../plugins/reporting/public/management/report_listing.tsx | 4 ++-- .../public/notifier/job_completion_notifications.ts | 4 ++-- .../plugins/reporting/server/browsers/safe_child_process.ts | 2 +- .../printable_pdf/create_job/compatibility_shim.ts | 6 +++--- x-pack/plugins/reporting/server/types.ts | 2 +- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/x-pack/plugins/reporting/public/management/report_listing.tsx b/x-pack/plugins/reporting/public/management/report_listing.tsx index 4e183380a6b41..c3a05042681c3 100644 --- a/x-pack/plugins/reporting/public/management/report_listing.tsx +++ b/x-pack/plugins/reporting/public/management/report_listing.tsx @@ -51,7 +51,7 @@ class ReportListingUi extends Component { private isInitialJobsFetch: boolean; private licenseSubscription?: Subscription; private mounted?: boolean; - private poller?: any; + private poller?: Poller; constructor(props: Props) { super(props); @@ -119,7 +119,7 @@ class ReportListingUi extends Component { public componentWillUnmount() { this.mounted = false; - this.poller.stop(); + this.poller?.stop(); if (this.licenseSubscription) { this.licenseSubscription.unsubscribe(); diff --git a/x-pack/plugins/reporting/public/notifier/job_completion_notifications.ts b/x-pack/plugins/reporting/public/notifier/job_completion_notifications.ts index e764f94105b70..c4addfa3eedef 100644 --- a/x-pack/plugins/reporting/public/notifier/job_completion_notifications.ts +++ b/x-pack/plugins/reporting/public/notifier/job_completion_notifications.ts @@ -9,11 +9,11 @@ import { JOB_COMPLETION_NOTIFICATIONS_SESSION_KEY } from '../../common/constants type JobId = string; -const set = (jobs: any) => { +const set = (jobs: string[]) => { sessionStorage.setItem(JOB_COMPLETION_NOTIFICATIONS_SESSION_KEY, JSON.stringify(jobs)); }; -const getAll = () => { +const getAll = (): string[] => { const sessionValue = sessionStorage.getItem(JOB_COMPLETION_NOTIFICATIONS_SESSION_KEY); return sessionValue ? JSON.parse(sessionValue) : []; }; diff --git a/x-pack/plugins/reporting/server/browsers/safe_child_process.ts b/x-pack/plugins/reporting/server/browsers/safe_child_process.ts index 9265dae23b896..70e45bf10803f 100644 --- a/x-pack/plugins/reporting/server/browsers/safe_child_process.ts +++ b/x-pack/plugins/reporting/server/browsers/safe_child_process.ts @@ -10,7 +10,7 @@ import { take, share, mapTo, delay, tap } from 'rxjs/operators'; import { LevelLogger } from '../lib'; interface IChild { - kill: (signal: string) => Promise; + kill: (signal: string) => Promise; } // Our process can get sent various signals, and when these occur we wish to diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/create_job/compatibility_shim.ts b/x-pack/plugins/reporting/server/export_types/printable_pdf/create_job/compatibility_shim.ts index f806b8a7e5bca..342e1fc7d85de 100644 --- a/x-pack/plugins/reporting/server/export_types/printable_pdf/create_job/compatibility_shim.ts +++ b/x-pack/plugins/reporting/server/export_types/printable_pdf/create_job/compatibility_shim.ts @@ -5,7 +5,7 @@ * 2.0. */ -import type { KibanaRequest } from 'kibana/server'; +import type { KibanaRequest, SavedObjectsClientContract } from 'kibana/server'; import { url as urlUtils } from '../../../../../../../src/plugins/kibana_utils/server'; import type { LevelLogger } from '../../../lib'; import type { CreateJobFn, ReportingRequestHandlerContext } from '../../../types'; @@ -20,9 +20,9 @@ function isLegacyJob( const getSavedObjectTitle = async ( objectType: string, savedObjectId: string, - savedObjectsClient: any + savedObjectsClient: SavedObjectsClientContract ) => { - const savedObject = await savedObjectsClient.get(objectType, savedObjectId); + const savedObject = await savedObjectsClient.get<{ title: string }>(objectType, savedObjectId); return savedObject.attributes.title; }; diff --git a/x-pack/plugins/reporting/server/types.ts b/x-pack/plugins/reporting/server/types.ts index 7fc638211e87b..406beb2a56b66 100644 --- a/x-pack/plugins/reporting/server/types.ts +++ b/x-pack/plugins/reporting/server/types.ts @@ -63,7 +63,7 @@ export { BaseParams, BasePayload }; export type CreateJobFn = ( jobParams: JobParamsType, context: ReportingRequestHandlerContext, - request: KibanaRequest + request: KibanaRequest ) => Promise; // default fn type for RunTaskFnFactory From 1ea921368fa1bd2c18965227164c831cf024c4d4 Mon Sep 17 00:00:00 2001 From: Tim Sullivan Date: Tue, 31 Aug 2021 11:09:07 -0700 Subject: [PATCH 10/81] [Reporting/Docs] Clarify reporting user access control options (#110545) * [Reporting/Docs] Clarify reporting user access control with kibana privileges * add reporting docs to code owners * Update docs/setup/configuring-reporting.asciidoc Co-authored-by: Kaarina Tungseth * Update docs/settings/reporting-settings.asciidoc Co-authored-by: Kaarina Tungseth * Update docs/setup/configuring-reporting.asciidoc Co-authored-by: Kaarina Tungseth * Update docs/setup/configuring-reporting.asciidoc Co-authored-by: Kaarina Tungseth Co-authored-by: Kaarina Tungseth --- .github/CODEOWNERS | 3 +++ docs/settings/reporting-settings.asciidoc | 13 ++++++------- docs/setup/configuring-reporting.asciidoc | 17 ++++++++++++----- 3 files changed, 21 insertions(+), 12 deletions(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 3829121aa5fe9..381fad404ca73 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -439,6 +439,9 @@ /x-pack/test/reporting_api_integration/ @elastic/kibana-reporting-services @elastic/kibana-app-services /x-pack/test/reporting_functional/ @elastic/kibana-reporting-services @elastic/kibana-app-services /x-pack/test/stack_functional_integration/apps/reporting/ @elastic/kibana-reporting-services @elastic/kibana-app-services +/docs/user/reporting @elastic/kibana-reporting-services @elastic/kibana-app-services +/docs/settings/reporting-settings.asciidoc @elastic/kibana-reporting-services @elastic/kibana-app-services +/docs/setup/configuring-reporting.asciidoc @elastic/kibana-reporting-services @elastic/kibana-app-services #CC# /x-pack/plugins/reporting/ @elastic/kibana-reporting-services diff --git a/docs/settings/reporting-settings.asciidoc b/docs/settings/reporting-settings.asciidoc index b339daf3d36f7..f215655f7f36f 100644 --- a/docs/settings/reporting-settings.asciidoc +++ b/docs/settings/reporting-settings.asciidoc @@ -281,16 +281,15 @@ NOTE: This setting exists for backwards compatibility, but is unused and hardcod [[reporting-advanced-settings]] ==== Security settings -[[xpack-reporting-roles-enabled]] `xpack.reporting.roles.enabled`:: -deprecated:[7.14.0,This setting must be set to `false` in 8.0.] When `true`, grants users access to the {report-features} by assigning reporting roles, specified by `xpack.reporting.roles.allow`. Granting access to users this way is deprecated. Set to `false` and use {kibana-ref}/kibana-privileges.html[{kib} privileges] instead. Defaults to `true`. +With Security enabled, Reporting has two forms of access control: each user can only access their own reports, and custom roles determine who has privilege to generate reports. When Reporting is configured with <>, you can control the spaces and applications where users are allowed to generate reports. [NOTE] ============================================================================ -In 7.x, the default value of `xpack.reporting.roles.enabled` is `true`. To migrate users to the -new method of securing access to *Reporting*, you must set `xpack.reporting.roles.enabled: false`. In the next major version of {kib}, `false` will be the only valid configuration. +The `xpack.reporting.roles` settings are for a deprecated system of access control in Reporting. It does not allow API Keys to generate reports, and it doesn't allow {kib} application privileges. We recommend you explicitly turn off reporting's deprecated access control feature by adding `xpack.reporting.roles.enabled: false` in kibana.yml. This will enable application privileges for reporting, as described in <>. ============================================================================ -`xpack.reporting.roles.allow`:: -deprecated:[7.14.0,This setting will be removed in 8.0.] Specifies the roles, in addition to superusers, that can generate reports, using the {ref}/security-api.html#security-role-apis[{es} role management APIs]. Requires `xpack.reporting.roles.enabled` to be `true`. Granting access to users this way is deprecated. Use {kibana-ref}/kibana-privileges.html[{kib} privileges] instead. Defaults to `[ "reporting_user" ]`. +[[xpack-reporting-roles-enabled]] `xpack.reporting.roles.enabled`:: +deprecated:[7.14.0,The default for this setting will be `false` in an upcoming version of {kib}.] Sets access control to a set of assigned reporting roles, specified by `xpack.reporting.roles.allow`. Defaults to `true`. -NOTE: Each user has access to only their own reports. +`xpack.reporting.roles.allow`:: +deprecated:[7.14.0] In addition to superusers, specifies the roles that can generate reports using the {ref}/security-api.html#security-role-apis[{es} role management APIs]. Requires `xpack.reporting.roles.enabled` to be `true`. Defaults to `[ "reporting_user" ]`. diff --git a/docs/setup/configuring-reporting.asciidoc b/docs/setup/configuring-reporting.asciidoc index 0dba7befa2931..6d209092d3338 100644 --- a/docs/setup/configuring-reporting.asciidoc +++ b/docs/setup/configuring-reporting.asciidoc @@ -41,11 +41,16 @@ To troubleshoot the problem, start the {kib} server with environment variables t [float] [[grant-user-access]] === Grant users access to reporting +When security is enabled, you grant users access to generate reports with <>, which allow you to create custom roles that control the spaces and applications where users generate reports. -When security is enabled, access to the {report-features} is controlled by roles and <>. With privileges, you can define custom roles that grant *Reporting* privileges as sub-features of {kib} applications. To grant users permission to generate reports and view their reports in *Reporting*, create and assign the reporting role. - -[[reporting-app-users]] -NOTE: In 7.12.0 and earlier, you grant access to the {report-features} by assigning users the `reporting_user` role in {es}. +. Enable application privileges in Reporting. To enable, turn off the default user access control features in `kibana.yml`: ++ +[source,yaml] +------------------------------------ +xpack.reporting.roles.enabled: false +------------------------------------ ++ +NOTE: If you use the default settings, you can still create a custom role that grants reporting privileges. The default role is `reporting_user`. This behavior is being deprecated and does not allow application-level access controls for {report-features}, and does not allow API keys or authentication tokens to authorize report generation. Refer to <> for information and caveats about the deprecated access control features. . Create the reporting role. @@ -90,10 +95,12 @@ If the *Reporting* option is unavailable, contact your administrator, or < Reporting*. Users can only access their own reports. + [float] [[reporting-roles-user-api]] ==== Grant access with the role API -You can also use the {ref}/security-api-put-role.html[role API] to grant access to the reporting features. Grant the reporting role to users in combination with other roles that grant read access to the data in {es}, and at least read access in the applications where users can generate reports. +With <> enabled in Reporting, you can also use the {ref}/security-api-put-role.html[role API] to grant access to the {report-features}. Grant custom reporting roles to users in combination with other roles that grant read access to the data in {es}, and at least read access in the applications where users can generate reports. [source, sh] --------------------------------------------------------------- From 23a178895f7fe2d762cd5587359fe4099118233b Mon Sep 17 00:00:00 2001 From: Jonathan Budzenski Date: Tue, 31 Aug 2021 13:17:50 -0500 Subject: [PATCH 11/81] [renovate] cleanup and disable dependency dashboard (#110664) --- renovate.json5 | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/renovate.json5 b/renovate.json5 index 5ea38e589da4d..b1464ad5040f0 100644 --- a/renovate.json5 +++ b/renovate.json5 @@ -1,6 +1,7 @@ { extends: [ 'config:base', + ':disableDependencyDashboard', ], ignorePaths: [ '**/__fixtures__/**', @@ -12,12 +13,11 @@ baseBranches: [ 'master', '7.x', - '7.13', + '7.15', ], prConcurrentLimit: 0, prHourlyLimit: 0, separateMajorMinor: false, - masterIssue: true, rangeStrategy: 'bump', semanticCommits: false, vulnerabilityAlerts: { @@ -39,7 +39,7 @@ packageNames: ['@elastic/charts'], reviewers: ['markov00', 'nickofthyme'], matchBaseBranches: ['master'], - labels: ['release_note:skip', 'v8.0.0', 'v7.14.0', 'auto-backport'], + labels: ['release_note:skip', 'v8.0.0', 'v7.16.0', 'auto-backport'], enabled: true, }, { From 45b13349dc0cbe1b2c0915820b167a0502a667d0 Mon Sep 17 00:00:00 2001 From: Jonathan Budzenski Date: Tue, 31 Aug 2021 15:02:07 -0500 Subject: [PATCH 12/81] Ignore elasticsearch-js product check warning by name (#110680) --- src/setup_node_env/exit_on_warning.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/setup_node_env/exit_on_warning.js b/src/setup_node_env/exit_on_warning.js index 5fbee02708083..e9c96f2c49bb4 100644 --- a/src/setup_node_env/exit_on_warning.js +++ b/src/setup_node_env/exit_on_warning.js @@ -34,8 +34,7 @@ var IGNORE_WARNINGS = [ // that the security features are blocking such check. // Such emit is causing Node.js to crash unless we explicitly catch it. // We need to discard that warning - message: - 'The client is unable to verify that the server is Elasticsearch due to security privileges on the server side. Some functionality may not be compatible if the server is running an unsupported product.', + name: 'ProductNotSupportedSecurityError', }, ]; From 01e1e4af3ff3480a9de3341c0d17cf2e12894b1d Mon Sep 17 00:00:00 2001 From: Domenico Andreoli Date: Tue, 31 Aug 2021 20:47:05 +0000 Subject: [PATCH 13/81] Merge junit results also for CCS functional tests (#110591) --- x-pack/plugins/security_solution/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/security_solution/package.json b/x-pack/plugins/security_solution/package.json index 3a0eb1a5458a8..5fda8730d5e9f 100644 --- a/x-pack/plugins/security_solution/package.json +++ b/x-pack/plugins/security_solution/package.json @@ -14,7 +14,7 @@ "cypress:run": "yarn cypress:run:reporter --browser chrome --headless --spec './cypress/integration/**/*.spec.ts'; status=$?; yarn junit:merge && exit $status", "cypress:run:firefox": "yarn cypress:run:reporter --browser firefox --headless --spec './cypress/integration/**/*.spec.ts'; status=$?; yarn junit:merge && exit $status", "cypress:run:reporter": "yarn cypress run --config-file ./cypress/cypress.json --reporter ../../../node_modules/cypress-multi-reporters --reporter-options configFile=./cypress/reporter_config.json", - "cypress:run:ccs": "yarn cypress:run:reporter --browser chrome --headless --config integrationFolder=./cypress/ccs_integration", + "cypress:run:ccs": "yarn cypress:run:reporter --browser chrome --headless --config integrationFolder=./cypress/ccs_integration; status=$?; yarn junit:merge && exit $status", "cypress:run-as-ci": "node --max-old-space-size=2048 ../../../scripts/functional_tests --config ../../test/security_solution_cypress/cli_config.ts", "cypress:run-as-ci:firefox": "node --max-old-space-size=2048 ../../../scripts/functional_tests --config ../../test/security_solution_cypress/config.firefox.ts", "cypress:run:upgrade": "yarn cypress:run:reporter --browser chrome --headless --config integrationFolder=./cypress/upgrade_integration", From 5e9617474ca86ca72d3b06008d3c1f5f236ab9a7 Mon Sep 17 00:00:00 2001 From: nastasha-solomon <79124755+nastasha-solomon@users.noreply.github.com> Date: Tue, 31 Aug 2021 17:20:00 -0400 Subject: [PATCH 14/81] [DOCS] Documenting securitySolution:defaultThreatIndex field (#110313) --- docs/management/advanced-options.asciidoc | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/docs/management/advanced-options.asciidoc b/docs/management/advanced-options.asciidoc index 49adc72bbe346..a4863bd60089b 100644 --- a/docs/management/advanced-options.asciidoc +++ b/docs/management/advanced-options.asciidoc @@ -186,7 +186,7 @@ Set to `true` to enable a dark mode for the {kib} UI. You must refresh the page to apply the setting. [[theme-version]]`theme:version`:: -Specifies the {kib} theme. If you change the setting, refresh the page to apply the setting. +Specifies the {kib} theme. If you change the setting, refresh the page to apply the setting. [[timepicker-quickranges]]`timepicker:quickRanges`:: The list of ranges to show in the Quick section of the time filter. This should @@ -214,7 +214,7 @@ truncation. When enabled, provides access to the experimental *Labs* features for *Canvas*. [[labs-dashboard-defer-below-fold]]`labs:dashboard:deferBelowFold`:: -When enabled, the panels that appear below the fold are loaded when they become visible on the dashboard. +When enabled, the panels that appear below the fold are loaded when they become visible on the dashboard. _Below the fold_ refers to panels that are not immediately visible when you open a dashboard, but become visible as you scroll. For additional information, refer to <>. [[labs-dashboard-enable-ui]]`labs:dashboard:enable_ui`:: @@ -240,7 +240,7 @@ Banners are a https://www.elastic.co/subscriptions[subscription feature]. [horizontal] [[banners-placement]]`banners:placement`:: -Set to `Top` to display a banner above the Elastic header for this space. Defaults to the value of +Set to `Top` to display a banner above the Elastic header for this space. Defaults to the value of the `xpack.banners.placement` configuration property. [[banners-textcontent]]`banners:textContent`:: @@ -443,6 +443,9 @@ The threshold above which {ml} job anomalies are displayed in the {security-app} A comma-delimited list of {es} indices from which the {security-app} collects events. +[[securitysolution-threatindices]]`securitySolution:defaultThreatIndex`:: +A comma-delimited list of Threat Intelligence indices from which the {security-app} collects indicators. + [[securitysolution-enablenewsfeed]]`securitySolution:enableNewsFeed`:: Enables the security news feed on the Security *Overview* page. @@ -544,4 +547,4 @@ only production-ready visualizations are available to users. [horizontal] [[telemetry-enabled-advanced-setting]]`telemetry:enabled`:: When enabled, helps improve the Elastic Stack by providing usage statistics for -basic features. This data will not be shared outside of Elastic. \ No newline at end of file +basic features. This data will not be shared outside of Elastic. From 522a0c4281b9b9ecf8a4d8885ab9736af5253f94 Mon Sep 17 00:00:00 2001 From: Kyle Pollich Date: Tue, 31 Aug 2021 17:43:00 -0400 Subject: [PATCH 15/81] [Fleet] Fix bug when upgrading Windows package policies (#110698) * Fix bug when upgrading Windows package policies Ensure package policy merge logics accounts for cases in which an input/stream which previously had no variables declared but has variables in a later package version. Fixes #110202 * Refactor original var set into deepMergeVars --- .../fleet/server/services/package_policy.ts | 22 ++-- .../test_stream/agent/stream/stream.yml.hbs | 1 + .../data_stream/test_stream/fields/fields.yml | 16 +++ .../data_stream/test_stream/manifest.yml | 4 + .../docs/README.md | 3 + .../manifest.yml | 23 ++++ .../test_stream/agent/stream/stream.yml.hbs | 1 + .../data_stream/test_stream/fields/fields.yml | 16 +++ .../data_stream/test_stream/manifest.yml | 17 +++ .../docs/README.md | 3 + .../manifest.yml | 23 ++++ .../apis/package_policy/upgrade.ts | 120 ++++++++++++++++++ 12 files changed, 238 insertions(+), 11 deletions(-) create mode 100644 x-pack/test/fleet_api_integration/apis/fixtures/test_packages/package_policy_upgrade/0.7.0-add-stream-with-no-vars/data_stream/test_stream/agent/stream/stream.yml.hbs create mode 100644 x-pack/test/fleet_api_integration/apis/fixtures/test_packages/package_policy_upgrade/0.7.0-add-stream-with-no-vars/data_stream/test_stream/fields/fields.yml create mode 100644 x-pack/test/fleet_api_integration/apis/fixtures/test_packages/package_policy_upgrade/0.7.0-add-stream-with-no-vars/data_stream/test_stream/manifest.yml create mode 100644 x-pack/test/fleet_api_integration/apis/fixtures/test_packages/package_policy_upgrade/0.7.0-add-stream-with-no-vars/docs/README.md create mode 100644 x-pack/test/fleet_api_integration/apis/fixtures/test_packages/package_policy_upgrade/0.7.0-add-stream-with-no-vars/manifest.yml create mode 100644 x-pack/test/fleet_api_integration/apis/fixtures/test_packages/package_policy_upgrade/0.8.0-add-vars-to-stream-with-no-vars/data_stream/test_stream/agent/stream/stream.yml.hbs create mode 100644 x-pack/test/fleet_api_integration/apis/fixtures/test_packages/package_policy_upgrade/0.8.0-add-vars-to-stream-with-no-vars/data_stream/test_stream/fields/fields.yml create mode 100644 x-pack/test/fleet_api_integration/apis/fixtures/test_packages/package_policy_upgrade/0.8.0-add-vars-to-stream-with-no-vars/data_stream/test_stream/manifest.yml create mode 100644 x-pack/test/fleet_api_integration/apis/fixtures/test_packages/package_policy_upgrade/0.8.0-add-vars-to-stream-with-no-vars/docs/README.md create mode 100644 x-pack/test/fleet_api_integration/apis/fixtures/test_packages/package_policy_upgrade/0.8.0-add-vars-to-stream-with-no-vars/manifest.yml diff --git a/x-pack/plugins/fleet/server/services/package_policy.ts b/x-pack/plugins/fleet/server/services/package_policy.ts index 6f74550ad45b7..8ff3c20b7aa15 100644 --- a/x-pack/plugins/fleet/server/services/package_policy.ts +++ b/x-pack/plugins/fleet/server/services/package_policy.ts @@ -944,7 +944,7 @@ export function overridePackageInputs( // If there's no corresponding input on the original package policy, just // take the override value from the new package as-is. This case typically // occurs when inputs or package policies are added/removed between versions. - if (!originalInput) { + if (originalInput === undefined) { inputs.push(override as NewPackagePolicyInput); continue; } @@ -958,7 +958,7 @@ export function overridePackageInputs( } if (override.vars) { - originalInput = deepMergeVars(originalInput, override); + originalInput = deepMergeVars(originalInput, override) as NewPackagePolicyInput; } if (override.streams) { @@ -967,6 +967,11 @@ export function overridePackageInputs( (s) => s.data_stream.dataset === stream.data_stream.dataset ); + if (originalStream === undefined) { + originalInput.streams.push(stream); + continue; + } + if (typeof stream.enabled !== 'undefined' && originalStream) { originalStream.enabled = stream.enabled; } @@ -1015,12 +1020,12 @@ export function overridePackageInputs( } function deepMergeVars(original: any, override: any): any { - const result = { ...original }; - - if (!result.vars || !override.vars) { - return; + if (!original.vars) { + original.vars = { ...override.vars }; } + const result = { ...original }; + const overrideVars = Array.isArray(override.vars) ? override.vars : Object.entries(override.vars!).map(([key, rest]) => ({ @@ -1030,11 +1035,6 @@ function deepMergeVars(original: any, override: any): any { for (const { name, ...overrideVal } of overrideVars) { const originalVar = original.vars[name]; - - if (!result.vars) { - result.vars = {}; - } - result.vars[name] = { ...overrideVal, ...originalVar }; } diff --git a/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/package_policy_upgrade/0.7.0-add-stream-with-no-vars/data_stream/test_stream/agent/stream/stream.yml.hbs b/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/package_policy_upgrade/0.7.0-add-stream-with-no-vars/data_stream/test_stream/agent/stream/stream.yml.hbs new file mode 100644 index 0000000000000..2870385f21f95 --- /dev/null +++ b/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/package_policy_upgrade/0.7.0-add-stream-with-no-vars/data_stream/test_stream/agent/stream/stream.yml.hbs @@ -0,0 +1 @@ +config.version: "2" diff --git a/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/package_policy_upgrade/0.7.0-add-stream-with-no-vars/data_stream/test_stream/fields/fields.yml b/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/package_policy_upgrade/0.7.0-add-stream-with-no-vars/data_stream/test_stream/fields/fields.yml new file mode 100644 index 0000000000000..6e003ed0ad147 --- /dev/null +++ b/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/package_policy_upgrade/0.7.0-add-stream-with-no-vars/data_stream/test_stream/fields/fields.yml @@ -0,0 +1,16 @@ +- name: data_stream.type + type: constant_keyword + description: > + Data stream type. +- name: data_stream.dataset + type: constant_keyword + description: > + Data stream dataset. +- name: data_stream.namespace + type: constant_keyword + description: > + Data stream namespace. +- name: '@timestamp' + type: date + description: > + Event timestamp. diff --git a/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/package_policy_upgrade/0.7.0-add-stream-with-no-vars/data_stream/test_stream/manifest.yml b/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/package_policy_upgrade/0.7.0-add-stream-with-no-vars/data_stream/test_stream/manifest.yml new file mode 100644 index 0000000000000..461d4fa941708 --- /dev/null +++ b/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/package_policy_upgrade/0.7.0-add-stream-with-no-vars/data_stream/test_stream/manifest.yml @@ -0,0 +1,4 @@ +title: Test stream +type: logs +streams: + - input: test_input diff --git a/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/package_policy_upgrade/0.7.0-add-stream-with-no-vars/docs/README.md b/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/package_policy_upgrade/0.7.0-add-stream-with-no-vars/docs/README.md new file mode 100644 index 0000000000000..0b9b18421c9dc --- /dev/null +++ b/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/package_policy_upgrade/0.7.0-add-stream-with-no-vars/docs/README.md @@ -0,0 +1,3 @@ +# Test package + +This is a test package for testing automated upgrades for package policies diff --git a/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/package_policy_upgrade/0.7.0-add-stream-with-no-vars/manifest.yml b/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/package_policy_upgrade/0.7.0-add-stream-with-no-vars/manifest.yml new file mode 100644 index 0000000000000..346ea4d2bcfad --- /dev/null +++ b/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/package_policy_upgrade/0.7.0-add-stream-with-no-vars/manifest.yml @@ -0,0 +1,23 @@ +format_version: 1.0.0 +name: package_policy_upgrade +title: Tests package policy upgrades +description: This is a test package for upgrading package policies +version: 0.7.0-add-stream-with-no-vars +categories: [] +release: beta +type: integration +license: basic +requirement: + elasticsearch: + versions: '>7.7.0' + kibana: + versions: '>7.7.0' +policy_templates: + - name: package_policy_upgrade_new + title: Package Policy Upgrade New + description: Test Package for Upgrading Package Policies + inputs: + - type: test_input + title: Test Input + description: Test Input + enabled: true diff --git a/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/package_policy_upgrade/0.8.0-add-vars-to-stream-with-no-vars/data_stream/test_stream/agent/stream/stream.yml.hbs b/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/package_policy_upgrade/0.8.0-add-vars-to-stream-with-no-vars/data_stream/test_stream/agent/stream/stream.yml.hbs new file mode 100644 index 0000000000000..2870385f21f95 --- /dev/null +++ b/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/package_policy_upgrade/0.8.0-add-vars-to-stream-with-no-vars/data_stream/test_stream/agent/stream/stream.yml.hbs @@ -0,0 +1 @@ +config.version: "2" diff --git a/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/package_policy_upgrade/0.8.0-add-vars-to-stream-with-no-vars/data_stream/test_stream/fields/fields.yml b/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/package_policy_upgrade/0.8.0-add-vars-to-stream-with-no-vars/data_stream/test_stream/fields/fields.yml new file mode 100644 index 0000000000000..6e003ed0ad147 --- /dev/null +++ b/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/package_policy_upgrade/0.8.0-add-vars-to-stream-with-no-vars/data_stream/test_stream/fields/fields.yml @@ -0,0 +1,16 @@ +- name: data_stream.type + type: constant_keyword + description: > + Data stream type. +- name: data_stream.dataset + type: constant_keyword + description: > + Data stream dataset. +- name: data_stream.namespace + type: constant_keyword + description: > + Data stream namespace. +- name: '@timestamp' + type: date + description: > + Event timestamp. diff --git a/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/package_policy_upgrade/0.8.0-add-vars-to-stream-with-no-vars/data_stream/test_stream/manifest.yml b/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/package_policy_upgrade/0.8.0-add-vars-to-stream-with-no-vars/data_stream/test_stream/manifest.yml new file mode 100644 index 0000000000000..8b8ea1987ccc3 --- /dev/null +++ b/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/package_policy_upgrade/0.8.0-add-vars-to-stream-with-no-vars/data_stream/test_stream/manifest.yml @@ -0,0 +1,17 @@ +title: Test stream +type: logs +streams: + - input: test_input + vars: + - name: test_var_new + type: text + title: Test Var New + default: Test Var New + required: true + show_user: true + - name: test_var_new_2 + type: text + title: Test Var New 2 + default: Test Var New 2 + required: true + show_user: true diff --git a/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/package_policy_upgrade/0.8.0-add-vars-to-stream-with-no-vars/docs/README.md b/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/package_policy_upgrade/0.8.0-add-vars-to-stream-with-no-vars/docs/README.md new file mode 100644 index 0000000000000..0b9b18421c9dc --- /dev/null +++ b/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/package_policy_upgrade/0.8.0-add-vars-to-stream-with-no-vars/docs/README.md @@ -0,0 +1,3 @@ +# Test package + +This is a test package for testing automated upgrades for package policies diff --git a/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/package_policy_upgrade/0.8.0-add-vars-to-stream-with-no-vars/manifest.yml b/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/package_policy_upgrade/0.8.0-add-vars-to-stream-with-no-vars/manifest.yml new file mode 100644 index 0000000000000..bd61453fdaac8 --- /dev/null +++ b/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/package_policy_upgrade/0.8.0-add-vars-to-stream-with-no-vars/manifest.yml @@ -0,0 +1,23 @@ +format_version: 1.0.0 +name: package_policy_upgrade +title: Tests package policy upgrades +description: This is a test package for upgrading package policies +version: 0.8.0-add-vars-to-stream-with-no-vars +categories: [] +release: beta +type: integration +license: basic +requirement: + elasticsearch: + versions: '>7.7.0' + kibana: + versions: '>7.7.0' +policy_templates: + - name: package_policy_upgrade_new + title: Package Policy Upgrade New + description: Test Package for Upgrading Package Policies + inputs: + - type: test_input + title: Test Input + description: Test Input + enabled: true diff --git a/x-pack/test/fleet_api_integration/apis/package_policy/upgrade.ts b/x-pack/test/fleet_api_integration/apis/package_policy/upgrade.ts index e75bcfaf75142..3a7d6f5d6b19e 100644 --- a/x-pack/test/fleet_api_integration/apis/package_policy/upgrade.ts +++ b/x-pack/test/fleet_api_integration/apis/package_policy/upgrade.ts @@ -803,6 +803,126 @@ export default function (providerContext: FtrProviderContext) { }); }); + describe('when upgrading to a version where an input with no variables has variables added', function () { + withTestPackageVersion('0.8.0-add-vars-to-stream-with-no-vars'); + + beforeEach(async function () { + const { body: agentPolicyResponse } = await supertest + .post(`/api/fleet/agent_policies`) + .set('kbn-xsrf', 'xxxx') + .send({ + name: 'Test policy', + namespace: 'default', + }) + .expect(200); + + agentPolicyId = agentPolicyResponse.item.id; + + const { body: packagePolicyResponse } = await supertest + .post(`/api/fleet/package_policies`) + .set('kbn-xsrf', 'xxxx') + .send({ + name: 'package_policy_upgrade_1', + description: '', + namespace: 'default', + policy_id: agentPolicyId, + enabled: true, + output_id: '', + inputs: [ + { + policy_template: 'package_policy_upgrade', + type: 'test_input', + enabled: true, + streams: [ + { + id: 'test-package_policy_upgrade-xxxx', + enabled: true, + data_stream: { + type: 'test_stream', + dataset: 'package_policy_upgrade.test_stream', + }, + }, + ], + }, + ], + package: { + name: 'package_policy_upgrade', + title: 'This is a test package for upgrading package policies', + version: '0.7.0-add-stream-with-no-vars', + }, + }); + + packagePolicyId = packagePolicyResponse.item.id; + }); + + afterEach(async function () { + await supertest + .post(`/api/fleet/package_policies/delete`) + .set('kbn-xsrf', 'xxxx') + .send({ packagePolicyIds: [packagePolicyId] }) + .expect(200); + + await supertest + .post('/api/fleet/agent_policies/delete') + .set('kbn-xsrf', 'xxxx') + .send({ agentPolicyId }) + .expect(200); + }); + + describe('dry run', function () { + it('returns a valid diff', async function () { + const { body }: { body: UpgradePackagePolicyDryRunResponse } = await supertest + .post(`/api/fleet/package_policies/upgrade`) + .set('kbn-xsrf', 'xxxx') + .send({ + packagePolicyIds: [packagePolicyId], + dryRun: true, + }) + .expect(200); + + expect(body[0].hasErrors).to.be(false); + + const oldInput = body[0].diff?.[0].inputs.find((input) => input.type === 'test_input'); + const oldStream = oldInput?.streams.find( + (stream) => stream.data_stream.dataset === 'package_policy_upgrade.test_stream' + ); + + expect(oldStream?.vars).to.be(undefined); + + const newInput = body[0].diff?.[1].inputs.find((input) => input.type === 'test_input'); + const newStream = newInput?.streams.find( + (stream) => stream.data_stream.dataset === 'package_policy_upgrade.test_stream' + ); + + expect(newStream?.vars).to.eql({ + test_var_new: { + value: 'Test Var New', + type: 'text', + }, + test_var_new_2: { + value: 'Test Var New 2', + type: 'text', + }, + }); + }); + }); + + describe('upgrade', function () { + it('successfully upgrades package policy', async function () { + const { body }: { body: UpgradePackagePolicyResponse } = await supertest + .post(`/api/fleet/package_policies/upgrade`) + .set('kbn-xsrf', 'xxxx') + .send({ + packagePolicyIds: [packagePolicyId], + dryRun: false, + }) + .expect(200); + + expect(body[0].success).to.be(true); + }); + }); + }); + describe('when package policy is not found', function () { it('should return an 200 with errors when "dryRun:true" is provided', async function () { const { body }: { body: UpgradePackagePolicyDryRunResponse } = await supertest From 72f6700270499127dfa1cc9a3f442b2883888169 Mon Sep 17 00:00:00 2001 From: Spencer Date: Tue, 31 Aug 2021 14:54:13 -0700 Subject: [PATCH 16/81] [eslint] prevent async Promise constructor mistakes (#110349) Co-authored-by: spalger --- package.json | 2 + .../elastic-eslint-config-kibana/.eslintrc.js | 2 + packages/kbn-eslint-plugin-eslint/index.js | 1 + .../rules/no_async_promise_body.js | 165 ++++++++ .../rules/no_async_promise_body.test.js | 254 ++++++++++++ .../application/integration_tests/utils.tsx | 17 +- .../application/ui/app_container.test.tsx | 8 +- .../ui/header/header_action_menu.test.tsx | 17 +- .../active_cursor/use_active_cursor.test.ts | 73 ++-- .../public/util/mount_point_portal.test.tsx | 17 +- .../lazy_load_bundle/get_service_settings.ts | 12 +- .../public/top_nav_menu/top_nav_menu.test.tsx | 17 +- .../public/legacy/vis_controller.ts | 61 +-- .../vislib/components/legend/legend.tsx | 29 +- .../public/lazy_load_bundle/index.ts | 14 +- .../public/lazy_load_bundle/index.ts | 21 +- .../mappings_editor/use_state_listener.tsx | 20 +- .../maps/public/lazy_load_bundle/index.ts | 63 +-- .../load_new_job_capabilities.ts | 45 ++- .../anomaly_charts_setup_flyout.tsx | 57 +-- .../anomaly_swimlane_setup_flyout.tsx | 57 +-- .../common/resolve_job_selection.tsx | 103 ++--- .../lib/logstash/get_paginated_pipelines.js | 101 +++-- .../session_management/session_index.ts | 106 ++--- .../routes/rules/import_rules_route.ts | 375 +++++++++--------- .../timelines/import_timelines/helpers.ts | 201 +++++----- .../configuration_statistics.test.ts | 106 ++--- .../monitoring/workload_statistics.test.ts | 57 ++- .../task_manager/server/task_scheduling.ts | 84 ++-- .../services/app_search_client.ts | 20 +- x-pack/test/lists_api_integration/utils.ts | 34 +- yarn.lock | 38 ++ 32 files changed, 1404 insertions(+), 773 deletions(-) create mode 100644 packages/kbn-eslint-plugin-eslint/rules/no_async_promise_body.js create mode 100644 packages/kbn-eslint-plugin-eslint/rules/no_async_promise_body.test.js diff --git a/package.json b/package.json index 836e5336b7b50..a5605371c2c45 100644 --- a/package.json +++ b/package.json @@ -655,6 +655,7 @@ "@types/yauzl": "^2.9.1", "@types/zen-observable": "^0.8.0", "@typescript-eslint/eslint-plugin": "^4.14.1", + "@typescript-eslint/typescript-estree": "^4.14.1", "@typescript-eslint/parser": "^4.14.1", "@yarnpkg/lockfile": "^1.1.0", "abab": "^2.0.4", @@ -725,6 +726,7 @@ "eslint-plugin-react": "^7.20.3", "eslint-plugin-react-hooks": "^4.2.0", "eslint-plugin-react-perf": "^3.2.3", + "eslint-traverse": "^1.0.0", "expose-loader": "^0.7.5", "faker": "^5.1.0", "fancy-log": "^1.3.2", diff --git a/packages/elastic-eslint-config-kibana/.eslintrc.js b/packages/elastic-eslint-config-kibana/.eslintrc.js index 1b3e852e5a502..38c0c43132564 100644 --- a/packages/elastic-eslint-config-kibana/.eslintrc.js +++ b/packages/elastic-eslint-config-kibana/.eslintrc.js @@ -90,5 +90,7 @@ module.exports = { }, ], ], + + '@kbn/eslint/no_async_promise_body': 'error', }, }; diff --git a/packages/kbn-eslint-plugin-eslint/index.js b/packages/kbn-eslint-plugin-eslint/index.js index e5a38e5f09529..a7a9c6b5bebdf 100644 --- a/packages/kbn-eslint-plugin-eslint/index.js +++ b/packages/kbn-eslint-plugin-eslint/index.js @@ -12,5 +12,6 @@ module.exports = { 'disallow-license-headers': require('./rules/disallow_license_headers'), 'no-restricted-paths': require('./rules/no_restricted_paths'), module_migration: require('./rules/module_migration'), + no_async_promise_body: require('./rules/no_async_promise_body'), }, }; diff --git a/packages/kbn-eslint-plugin-eslint/rules/no_async_promise_body.js b/packages/kbn-eslint-plugin-eslint/rules/no_async_promise_body.js new file mode 100644 index 0000000000000..317758fd3629a --- /dev/null +++ b/packages/kbn-eslint-plugin-eslint/rules/no_async_promise_body.js @@ -0,0 +1,165 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +const { parseExpression } = require('@babel/parser'); +const { default: generate } = require('@babel/generator'); +const tsEstree = require('@typescript-eslint/typescript-estree'); +const traverse = require('eslint-traverse'); +const esTypes = tsEstree.AST_NODE_TYPES; +const babelTypes = require('@babel/types'); + +/** @typedef {import("eslint").Rule.RuleModule} Rule */ +/** @typedef {import("@typescript-eslint/parser").ParserServices} ParserServices */ +/** @typedef {import("@typescript-eslint/typescript-estree").TSESTree.Expression} Expression */ +/** @typedef {import("@typescript-eslint/typescript-estree").TSESTree.ArrowFunctionExpression} ArrowFunctionExpression */ +/** @typedef {import("@typescript-eslint/typescript-estree").TSESTree.FunctionExpression} FunctionExpression */ +/** @typedef {import("@typescript-eslint/typescript-estree").TSESTree.TryStatement} TryStatement */ +/** @typedef {import("@typescript-eslint/typescript-estree").TSESTree.NewExpression} NewExpression */ +/** @typedef {import("typescript").ExportDeclaration} ExportDeclaration */ +/** @typedef {import("eslint").Rule.RuleFixer} Fixer */ + +const ERROR_MSG = + 'Passing an async function to the Promise constructor leads to a hidden promise being created and prevents handling rejections'; + +/** + * @param {Expression} node + */ +const isPromise = (node) => node.type === esTypes.Identifier && node.name === 'Promise'; + +/** + * @param {Expression} node + * @returns {node is ArrowFunctionExpression | FunctionExpression} + */ +const isFunc = (node) => + node.type === esTypes.ArrowFunctionExpression || node.type === esTypes.FunctionExpression; + +/** + * @param {any} context + * @param {ArrowFunctionExpression | FunctionExpression} node + */ +const isFuncBodySafe = (context, node) => { + // if the body isn't wrapped in a blockStatement it can't have a try/catch at the root + if (node.body.type !== esTypes.BlockStatement) { + return false; + } + + // when the entire body is wrapped in a try/catch it is the only node + if (node.body.body.length !== 1) { + return false; + } + + const tryNode = node.body.body[0]; + // ensure we have a try node with a handler + if (tryNode.type !== esTypes.TryStatement || !tryNode.handler) { + return false; + } + + // ensure the handler doesn't throw + let hasThrow = false; + traverse(context, tryNode.handler, (path) => { + if (path.node.type === esTypes.ThrowStatement) { + hasThrow = true; + return traverse.STOP; + } + }); + return !hasThrow; +}; + +/** + * @param {string} code + */ +const wrapFunctionInTryCatch = (code) => { + // parse the code with babel so we can mutate the AST + const ast = parseExpression(code, { + plugins: ['typescript', 'jsx'], + }); + + // validate that the code reperesents an arrow or function expression + if (!babelTypes.isArrowFunctionExpression(ast) && !babelTypes.isFunctionExpression(ast)) { + throw new Error('expected function to be an arrow or function expression'); + } + + // ensure that the function receives the second argument, and capture its name if already defined + let rejectName = 'reject'; + if (ast.params.length === 0) { + ast.params.push(babelTypes.identifier('resolve'), babelTypes.identifier(rejectName)); + } else if (ast.params.length === 1) { + ast.params.push(babelTypes.identifier(rejectName)); + } else if (ast.params.length === 2) { + if (babelTypes.isIdentifier(ast.params[1])) { + rejectName = ast.params[1].name; + } else { + throw new Error('expected second param of promise definition function to be an identifier'); + } + } + + // ensure that the body of the function is a blockStatement + let block = ast.body; + if (!babelTypes.isBlockStatement(block)) { + block = babelTypes.blockStatement([babelTypes.returnStatement(block)]); + } + + // redefine the body of the function as a new blockStatement containing a tryStatement + // which catches errors and forwards them to reject() when caught + ast.body = babelTypes.blockStatement([ + // try { + babelTypes.tryStatement( + block, + // catch (error) { + babelTypes.catchClause( + babelTypes.identifier('error'), + babelTypes.blockStatement([ + // reject(error) + babelTypes.expressionStatement( + babelTypes.callExpression(babelTypes.identifier(rejectName), [ + babelTypes.identifier('error'), + ]) + ), + ]) + ) + ), + ]); + + return generate(ast).code; +}; + +/** @type {Rule} */ +module.exports = { + meta: { + fixable: 'code', + schema: [], + }, + create: (context) => ({ + NewExpression(_) { + const node = /** @type {NewExpression} */ (_); + + // ensure we are newing up a promise with a single argument + if (!isPromise(node.callee) || node.arguments.length !== 1) { + return; + } + + const func = node.arguments[0]; + // ensure the argument is an arrow or function expression and is async + if (!isFunc(func) || !func.async) { + return; + } + + // body must be a blockStatement, try/catch can't exist outside of a block + if (!isFuncBodySafe(context, func)) { + context.report({ + message: ERROR_MSG, + loc: func.loc, + fix(fixer) { + const source = context.getSourceCode(); + return fixer.replaceText(func, wrapFunctionInTryCatch(source.getText(func))); + }, + }); + } + }, + }), +}; diff --git a/packages/kbn-eslint-plugin-eslint/rules/no_async_promise_body.test.js b/packages/kbn-eslint-plugin-eslint/rules/no_async_promise_body.test.js new file mode 100644 index 0000000000000..f5929b1b3966f --- /dev/null +++ b/packages/kbn-eslint-plugin-eslint/rules/no_async_promise_body.test.js @@ -0,0 +1,254 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +const { RuleTester } = require('eslint'); +const rule = require('./no_async_promise_body'); +const dedent = require('dedent'); + +const ruleTester = new RuleTester({ + parser: require.resolve('@typescript-eslint/parser'), + parserOptions: { + sourceType: 'module', + ecmaVersion: 2018, + ecmaFeatures: { + jsx: true, + }, + }, +}); + +ruleTester.run('@kbn/eslint/no_async_promise_body', rule, { + valid: [ + // caught but no resolve + { + code: dedent` + new Promise(async function (resolve) { + try { + await asyncOperation(); + } catch (error) { + // noop + } + }) + `, + }, + // arrow caught but no resolve + { + code: dedent` + new Promise(async (resolve) => { + try { + await asyncOperation(); + } catch (error) { + // noop + } + }) + `, + }, + // caught with reject + { + code: dedent` + new Promise(async function (resolve, reject) { + try { + await asyncOperation(); + } catch (error) { + reject(error) + } + }) + `, + }, + // arrow caught with reject + { + code: dedent` + new Promise(async (resolve, reject) => { + try { + await asyncOperation(); + } catch (error) { + reject(error) + } + }) + `, + }, + // non async + { + code: dedent` + new Promise(function (resolve) { + setTimeout(resolve, 10); + }) + `, + }, + // arrow non async + { + code: dedent` + new Promise((resolve) => setTimeout(resolve, 10)) + `, + }, + ], + + invalid: [ + // no catch + { + code: dedent` + new Promise(async function (resolve) { + const result = await asyncOperation(); + resolve(result); + }) + `, + errors: [ + { + line: 1, + message: + 'Passing an async function to the Promise constructor leads to a hidden promise being created and prevents handling rejections', + }, + ], + output: dedent` + new Promise(async function (resolve, reject) { + try { + const result = await asyncOperation(); + resolve(result); + } catch (error) { + reject(error); + } + }) + `, + }, + // arrow no catch + { + code: dedent` + new Promise(async (resolve) => { + const result = await asyncOperation(); + resolve(result); + }) + `, + errors: [ + { + line: 1, + message: + 'Passing an async function to the Promise constructor leads to a hidden promise being created and prevents handling rejections', + }, + ], + output: dedent` + new Promise(async (resolve, reject) => { + try { + const result = await asyncOperation(); + resolve(result); + } catch (error) { + reject(error); + } + }) + `, + }, + // catch, but it throws + { + code: dedent` + new Promise(async function (resolve) { + try { + const result = await asyncOperation(); + resolve(result); + } catch (error) { + if (error.code === 'foo') { + throw error; + } + } + }) + `, + errors: [ + { + line: 1, + message: + 'Passing an async function to the Promise constructor leads to a hidden promise being created and prevents handling rejections', + }, + ], + output: dedent` + new Promise(async function (resolve, reject) { + try { + try { + const result = await asyncOperation(); + resolve(result); + } catch (error) { + if (error.code === 'foo') { + throw error; + } + } + } catch (error) { + reject(error); + } + }) + `, + }, + // no catch without block + { + code: dedent` + new Promise(async (resolve) => resolve(await asyncOperation())); + `, + errors: [ + { + line: 1, + message: + 'Passing an async function to the Promise constructor leads to a hidden promise being created and prevents handling rejections', + }, + ], + output: dedent` + new Promise(async (resolve, reject) => { + try { + return resolve(await asyncOperation()); + } catch (error) { + reject(error); + } + }); + `, + }, + // no catch with named reject + { + code: dedent` + new Promise(async (resolve, rej) => { + const result = await asyncOperation(); + result ? resolve(true) : rej() + }); + `, + errors: [ + { + line: 1, + message: + 'Passing an async function to the Promise constructor leads to a hidden promise being created and prevents handling rejections', + }, + ], + output: dedent` + new Promise(async (resolve, rej) => { + try { + const result = await asyncOperation(); + result ? resolve(true) : rej(); + } catch (error) { + rej(error); + } + }); + `, + }, + // no catch with no args + { + code: dedent` + new Promise(async () => { + await asyncOperation(); + }); + `, + errors: [ + { + line: 1, + message: + 'Passing an async function to the Promise constructor leads to a hidden promise being created and prevents handling rejections', + }, + ], + output: dedent` + new Promise(async (resolve, reject) => { + try { + await asyncOperation(); + } catch (error) { + reject(error); + } + }); + `, + }, + ], +}); diff --git a/src/core/public/application/integration_tests/utils.tsx b/src/core/public/application/integration_tests/utils.tsx index dcf071719c11a..455d19956f7e8 100644 --- a/src/core/public/application/integration_tests/utils.tsx +++ b/src/core/public/application/integration_tests/utils.tsx @@ -21,13 +21,18 @@ export const createRenderer = (element: ReactElement | null): Renderer => { const dom: Dom = element && mount({element}); return () => - new Promise(async (resolve) => { - if (dom) { - await act(async () => { - dom.update(); - }); + new Promise(async (resolve, reject) => { + try { + if (dom) { + await act(async () => { + dom.update(); + }); + } + + setImmediate(() => resolve(dom)); // flushes any pending promises + } catch (error) { + reject(error); } - setImmediate(() => resolve(dom)); // flushes any pending promises }); }; diff --git a/src/core/public/application/ui/app_container.test.tsx b/src/core/public/application/ui/app_container.test.tsx index 86cb9198e0699..4c056e748f06e 100644 --- a/src/core/public/application/ui/app_container.test.tsx +++ b/src/core/public/application/ui/app_container.test.tsx @@ -27,8 +27,12 @@ describe('AppContainer', () => { }); const flushPromises = async () => { - await new Promise(async (resolve) => { - setImmediate(() => resolve()); + await new Promise(async (resolve, reject) => { + try { + setImmediate(() => resolve()); + } catch (error) { + reject(error); + } }); }; diff --git a/src/core/public/chrome/ui/header/header_action_menu.test.tsx b/src/core/public/chrome/ui/header/header_action_menu.test.tsx index 386e48e745e80..201be8848bac8 100644 --- a/src/core/public/chrome/ui/header/header_action_menu.test.tsx +++ b/src/core/public/chrome/ui/header/header_action_menu.test.tsx @@ -26,13 +26,18 @@ describe('HeaderActionMenu', () => { }); const refresh = () => { - new Promise(async (resolve) => { - if (component) { - act(() => { - component.update(); - }); + new Promise(async (resolve, reject) => { + try { + if (component) { + act(() => { + component.update(); + }); + } + + setImmediate(() => resolve(component)); // flushes any pending promises + } catch (error) { + reject(error); } - setImmediate(() => resolve(component)); // flushes any pending promises }); }; diff --git a/src/plugins/charts/public/services/active_cursor/use_active_cursor.test.ts b/src/plugins/charts/public/services/active_cursor/use_active_cursor.test.ts index efe5c9b49849f..8bc78956a0919 100644 --- a/src/plugins/charts/public/services/active_cursor/use_active_cursor.test.ts +++ b/src/plugins/charts/public/services/active_cursor/use_active_cursor.test.ts @@ -24,42 +24,47 @@ describe('useActiveCursor', () => { events: Array>, eventsTimeout = 1 ) => - new Promise(async (resolve) => { - const activeCursor = new ActiveCursor(); - let allEventsExecuted = false; - - activeCursor.setup(); + new Promise(async (resolve, reject) => { + try { + const activeCursor = new ActiveCursor(); + let allEventsExecuted = false; + activeCursor.setup(); + dispatchExternalPointerEvent.mockImplementation((pointerEvent) => { + if (allEventsExecuted) { + resolve(pointerEvent); + } + }); + renderHook(() => + useActiveCursor( + activeCursor, + { + current: { + dispatchExternalPointerEvent: dispatchExternalPointerEvent as ( + pointerEvent: PointerEvent + ) => void, + }, + } as RefObject, + { ...syncOption, debounce: syncOption.debounce ?? 1 } + ) + ); - dispatchExternalPointerEvent.mockImplementation((pointerEvent) => { - if (allEventsExecuted) { - resolve(pointerEvent); + for (const e of events) { + await new Promise((eventResolve) => + setTimeout(() => { + if (e === events[events.length - 1]) { + allEventsExecuted = true; + } + + activeCursor.activeCursor$!.next({ + cursor, + ...e, + }); + eventResolve(null); + }, eventsTimeout) + ); } - }); - - renderHook(() => - useActiveCursor( - activeCursor, - { - current: { - dispatchExternalPointerEvent: dispatchExternalPointerEvent as ( - pointerEvent: PointerEvent - ) => void, - }, - } as RefObject, - { ...syncOption, debounce: syncOption.debounce ?? 1 } - ) - ); - - for (const e of events) { - await new Promise((eventResolve) => - setTimeout(() => { - if (e === events[events.length - 1]) { - allEventsExecuted = true; - } - activeCursor.activeCursor$!.next({ cursor, ...e }); - eventResolve(null); - }, eventsTimeout) - ); + } catch (error) { + reject(error); } }); diff --git a/src/plugins/kibana_react/public/util/mount_point_portal.test.tsx b/src/plugins/kibana_react/public/util/mount_point_portal.test.tsx index 53503b197567e..39e345568a298 100644 --- a/src/plugins/kibana_react/public/util/mount_point_portal.test.tsx +++ b/src/plugins/kibana_react/public/util/mount_point_portal.test.tsx @@ -19,13 +19,18 @@ describe('MountPointPortal', () => { let dom: ReactWrapper; const refresh = () => { - new Promise(async (resolve) => { - if (dom) { - act(() => { - dom.update(); - }); + new Promise(async (resolve, reject) => { + try { + if (dom) { + act(() => { + dom.update(); + }); + } + + setImmediate(() => resolve(dom)); // flushes any pending promises + } catch (error) { + reject(error); } - setImmediate(() => resolve(dom)); // flushes any pending promises }); }; diff --git a/src/plugins/maps_ems/public/lazy_load_bundle/get_service_settings.ts b/src/plugins/maps_ems/public/lazy_load_bundle/get_service_settings.ts index 6e32ff5d4e41e..8eafada176e7a 100644 --- a/src/plugins/maps_ems/public/lazy_load_bundle/get_service_settings.ts +++ b/src/plugins/maps_ems/public/lazy_load_bundle/get_service_settings.ts @@ -16,10 +16,14 @@ export async function getServiceSettings(): Promise { return loadPromise; } - loadPromise = new Promise(async (resolve) => { - const { ServiceSettings } = await import('./lazy'); - const config = getMapsEmsConfig(); - resolve(new ServiceSettings(config, config.tilemap)); + loadPromise = new Promise(async (resolve, reject) => { + try { + const { ServiceSettings } = await import('./lazy'); + const config = getMapsEmsConfig(); + resolve(new ServiceSettings(config, config.tilemap)); + } catch (error) { + reject(error); + } }); return loadPromise; } diff --git a/src/plugins/navigation/public/top_nav_menu/top_nav_menu.test.tsx b/src/plugins/navigation/public/top_nav_menu/top_nav_menu.test.tsx index 558739a44dd41..45b9b4c7a885b 100644 --- a/src/plugins/navigation/public/top_nav_menu/top_nav_menu.test.tsx +++ b/src/plugins/navigation/public/top_nav_menu/top_nav_menu.test.tsx @@ -109,13 +109,18 @@ describe('TopNavMenu', () => { let dom: ReactWrapper; const refresh = () => { - new Promise(async (resolve) => { - if (dom) { - act(() => { - dom.update(); - }); + new Promise(async (resolve, reject) => { + try { + if (dom) { + act(() => { + dom.update(); + }); + } + + setImmediate(() => resolve(dom)); // flushes any pending promises + } catch (error) { + reject(error); } - setImmediate(() => resolve(dom)); // flushes any pending promises }); }; diff --git a/src/plugins/vis_type_table/public/legacy/vis_controller.ts b/src/plugins/vis_type_table/public/legacy/vis_controller.ts index ec198aa96f1f9..a9cb22a056913 100644 --- a/src/plugins/vis_type_table/public/legacy/vis_controller.ts +++ b/src/plugins/vis_type_table/public/legacy/vis_controller.ts @@ -71,35 +71,42 @@ export function getTableVisualizationControllerClass( await this.initLocalAngular(); return new Promise(async (resolve, reject) => { - if (!this.$rootScope) { - const $injector = this.getInjector(); - this.$rootScope = $injector.get('$rootScope'); - this.$compile = $injector.get('$compile'); - } - const updateScope = () => { - if (!this.$scope) { - return; + try { + if (!this.$rootScope) { + const $injector = this.getInjector(); + this.$rootScope = $injector.get('$rootScope'); + this.$compile = $injector.get('$compile'); } - this.$scope.visState = { params: visParams, title: visParams.title }; - this.$scope.esResponse = esResponse; - - this.$scope.visParams = visParams; - this.$scope.renderComplete = resolve; - this.$scope.renderFailed = reject; - this.$scope.resize = Date.now(); - this.$scope.$apply(); - }; - - if (!this.$scope && this.$compile) { - this.$scope = this.$rootScope.$new(); - this.$scope.uiState = handlers.uiState; - this.$scope.filter = handlers.event; - updateScope(); - this.el.find('div').append(this.$compile(tableVisTemplate)(this.$scope)); - this.$scope.$apply(); - } else { - updateScope(); + const updateScope = () => { + if (!this.$scope) { + return; + } + + this.$scope.visState = { + params: visParams, + title: visParams.title, + }; + this.$scope.esResponse = esResponse; + this.$scope.visParams = visParams; + this.$scope.renderComplete = resolve; + this.$scope.renderFailed = reject; + this.$scope.resize = Date.now(); + this.$scope.$apply(); + }; + + if (!this.$scope && this.$compile) { + this.$scope = this.$rootScope.$new(); + this.$scope.uiState = handlers.uiState; + this.$scope.filter = handlers.event; + updateScope(); + this.el.find('div').append(this.$compile(tableVisTemplate)(this.$scope)); + this.$scope.$apply(); + } else { + updateScope(); + } + } catch (error) { + reject(error); } }); } diff --git a/src/plugins/vis_types/vislib/public/vislib/components/legend/legend.tsx b/src/plugins/vis_types/vislib/public/vislib/components/legend/legend.tsx index 56f9025a6bd0b..4701d07ab83e6 100644 --- a/src/plugins/vis_types/vislib/public/vislib/components/legend/legend.tsx +++ b/src/plugins/vis_types/vislib/public/vislib/components/legend/legend.tsx @@ -124,16 +124,25 @@ export class VisLegend extends PureComponent { }; setFilterableLabels = (items: LegendItem[]): Promise => - new Promise(async (resolve) => { - const filterableLabels = new Set(); - items.forEach(async (item) => { - const canFilter = await this.canFilter(item); - if (canFilter) { - filterableLabels.add(item.label); - } - }); - - this.setState({ filterableLabels }, resolve); + new Promise(async (resolve, reject) => { + try { + const filterableLabels = new Set(); + items.forEach(async (item) => { + const canFilter = await this.canFilter(item); + + if (canFilter) { + filterableLabels.add(item.label); + } + }); + this.setState( + { + filterableLabels, + }, + resolve + ); + } catch (error) { + reject(error); + } }); setLabels = (data: any, type: string) => { diff --git a/x-pack/plugins/data_visualizer/public/lazy_load_bundle/index.ts b/x-pack/plugins/data_visualizer/public/lazy_load_bundle/index.ts index 57f0872d62589..f04c611c2fae9 100644 --- a/x-pack/plugins/data_visualizer/public/lazy_load_bundle/index.ts +++ b/x-pack/plugins/data_visualizer/public/lazy_load_bundle/index.ts @@ -22,13 +22,13 @@ export async function lazyLoadModules(): Promise { return loadModulesPromise; } - loadModulesPromise = new Promise(async (resolve) => { - const lazyImports = await import('./lazy'); - - resolve({ - ...lazyImports, - getHttp: () => getCoreStart().http, - }); + loadModulesPromise = new Promise(async (resolve, reject) => { + try { + const lazyImports = await import('./lazy'); + resolve({ ...lazyImports, getHttp: () => getCoreStart().http }); + } catch (error) { + reject(error); + } }); return loadModulesPromise; } diff --git a/x-pack/plugins/file_upload/public/lazy_load_bundle/index.ts b/x-pack/plugins/file_upload/public/lazy_load_bundle/index.ts index 9c7c6ff1e5180..192a7ffb5e782 100644 --- a/x-pack/plugins/file_upload/public/lazy_load_bundle/index.ts +++ b/x-pack/plugins/file_upload/public/lazy_load_bundle/index.ts @@ -44,15 +44,18 @@ export async function lazyLoadModules(): Promise { return loadModulesPromise; } - loadModulesPromise = new Promise(async (resolve) => { - const { JsonUploadAndParse, importerFactory, IndexNameForm } = await import('./lazy'); - - resolve({ - JsonUploadAndParse, - importerFactory, - getHttp, - IndexNameForm, - }); + loadModulesPromise = new Promise(async (resolve, reject) => { + try { + const { JsonUploadAndParse, importerFactory, IndexNameForm } = await import('./lazy'); + resolve({ + JsonUploadAndParse, + importerFactory, + getHttp, + IndexNameForm, + }); + } catch (error) { + reject(error); + } }); return loadModulesPromise; } diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/use_state_listener.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/use_state_listener.tsx index a8ccb0f5119c8..e7ace1aff3101 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/use_state_listener.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/use_state_listener.tsx @@ -94,17 +94,25 @@ export const useMappingsStateListener = ({ onChange, value }: Args) => { validate: async () => { const configurationFormValidator = state.configuration.submitForm !== undefined - ? new Promise(async (resolve) => { - const { isValid } = await state.configuration.submitForm!(); - resolve(isValid); + ? new Promise(async (resolve, reject) => { + try { + const { isValid } = await state.configuration.submitForm!(); + resolve(isValid); + } catch (error) { + reject(error); + } }) : Promise.resolve(true); const templatesFormValidator = state.templates.submitForm !== undefined - ? new Promise(async (resolve) => { - const { isValid } = await state.templates.submitForm!(); - resolve(isValid); + ? new Promise(async (resolve, reject) => { + try { + const { isValid } = await state.templates.submitForm!(); + resolve(isValid); + } catch (error) { + reject(error); + } }) : Promise.resolve(true); diff --git a/x-pack/plugins/maps/public/lazy_load_bundle/index.ts b/x-pack/plugins/maps/public/lazy_load_bundle/index.ts index 788e5938ee168..7157b145f828d 100644 --- a/x-pack/plugins/maps/public/lazy_load_bundle/index.ts +++ b/x-pack/plugins/maps/public/lazy_load_bundle/index.ts @@ -70,36 +70,39 @@ export async function lazyLoadMapModules(): Promise { return loadModulesPromise; } - loadModulesPromise = new Promise(async (resolve) => { - const { - MapEmbeddable, - getIndexPatternService, - getMapsCapabilities, - renderApp, - createSecurityLayerDescriptors, - registerLayerWizard, - registerSource, - createTileMapLayerDescriptor, - createRegionMapLayerDescriptor, - createBasemapLayerDescriptor, - createESSearchSourceLayerDescriptor, - suggestEMSTermJoinConfig, - } = await import('./lazy'); - - resolve({ - MapEmbeddable, - getIndexPatternService, - getMapsCapabilities, - renderApp, - createSecurityLayerDescriptors, - registerLayerWizard, - registerSource, - createTileMapLayerDescriptor, - createRegionMapLayerDescriptor, - createBasemapLayerDescriptor, - createESSearchSourceLayerDescriptor, - suggestEMSTermJoinConfig, - }); + loadModulesPromise = new Promise(async (resolve, reject) => { + try { + const { + MapEmbeddable, + getIndexPatternService, + getMapsCapabilities, + renderApp, + createSecurityLayerDescriptors, + registerLayerWizard, + registerSource, + createTileMapLayerDescriptor, + createRegionMapLayerDescriptor, + createBasemapLayerDescriptor, + createESSearchSourceLayerDescriptor, + suggestEMSTermJoinConfig, + } = await import('./lazy'); + resolve({ + MapEmbeddable, + getIndexPatternService, + getMapsCapabilities, + renderApp, + createSecurityLayerDescriptors, + registerLayerWizard, + registerSource, + createTileMapLayerDescriptor, + createRegionMapLayerDescriptor, + createBasemapLayerDescriptor, + createESSearchSourceLayerDescriptor, + suggestEMSTermJoinConfig, + }); + } catch (error) { + reject(error); + } }); return loadModulesPromise; } diff --git a/x-pack/plugins/ml/public/application/services/new_job_capabilities/load_new_job_capabilities.ts b/x-pack/plugins/ml/public/application/services/new_job_capabilities/load_new_job_capabilities.ts index a998343535249..d3b407c2bb65a 100644 --- a/x-pack/plugins/ml/public/application/services/new_job_capabilities/load_new_job_capabilities.ts +++ b/x-pack/plugins/ml/public/application/services/new_job_capabilities/load_new_job_capabilities.ts @@ -23,27 +23,34 @@ export function loadNewJobCapabilities( jobType: JobType ) { return new Promise(async (resolve, reject) => { - const serviceToUse = - jobType === ANOMALY_DETECTOR ? newJobCapsService : newJobCapsServiceAnalytics; - if (indexPatternId !== undefined) { - // index pattern is being used - const indexPattern: IIndexPattern = await indexPatterns.get(indexPatternId); - await serviceToUse.initializeFromIndexPattern(indexPattern); - resolve(serviceToUse.newJobCaps); - } else if (savedSearchId !== undefined) { - // saved search is being used - // load the index pattern from the saved search - const { indexPattern } = await getIndexPatternAndSavedSearch(savedSearchId); - if (indexPattern === null) { - // eslint-disable-next-line no-console - console.error('Cannot retrieve index pattern from saved search'); + try { + const serviceToUse = + jobType === ANOMALY_DETECTOR ? newJobCapsService : newJobCapsServiceAnalytics; + + if (indexPatternId !== undefined) { + // index pattern is being used + const indexPattern: IIndexPattern = await indexPatterns.get(indexPatternId); + await serviceToUse.initializeFromIndexPattern(indexPattern); + resolve(serviceToUse.newJobCaps); + } else if (savedSearchId !== undefined) { + // saved search is being used + // load the index pattern from the saved search + const { indexPattern } = await getIndexPatternAndSavedSearch(savedSearchId); + + if (indexPattern === null) { + // eslint-disable-next-line no-console + console.error('Cannot retrieve index pattern from saved search'); + reject(); + return; + } + + await serviceToUse.initializeFromIndexPattern(indexPattern); + resolve(serviceToUse.newJobCaps); + } else { reject(); - return; } - await serviceToUse.initializeFromIndexPattern(indexPattern); - resolve(serviceToUse.newJobCaps); - } else { - reject(); + } catch (error) { + reject(error); } }); } diff --git a/x-pack/plugins/ml/public/embeddables/anomaly_charts/anomaly_charts_setup_flyout.tsx b/x-pack/plugins/ml/public/embeddables/anomaly_charts/anomaly_charts_setup_flyout.tsx index eb39ba4ab29aa..5090274ca7383 100644 --- a/x-pack/plugins/ml/public/embeddables/anomaly_charts/anomaly_charts_setup_flyout.tsx +++ b/x-pack/plugins/ml/public/embeddables/anomaly_charts/anomaly_charts_setup_flyout.tsx @@ -25,33 +25,34 @@ export async function resolveEmbeddableAnomalyChartsUserInput( const anomalyDetectorService = new AnomalyDetectorService(new HttpService(http)); return new Promise(async (resolve, reject) => { - const { jobIds } = await resolveJobSelection(coreStart, input?.jobIds); - - const title = input?.title ?? getDefaultExplorerChartsPanelTitle(jobIds); - const jobs = await anomalyDetectorService.getJobs$(jobIds).toPromise(); - const influencers = anomalyDetectorService.extractInfluencers(jobs); - influencers.push(VIEW_BY_JOB_LABEL); - - const modalSession = overlays.openModal( - toMountPoint( - { - modalSession.close(); - - resolve({ - jobIds, - title: panelTitle, - maxSeriesToPlot, - }); - }} - onCancel={() => { - modalSession.close(); - reject(); - }} - /> - ) - ); + try { + const { jobIds } = await resolveJobSelection(coreStart, input?.jobIds); + const title = input?.title ?? getDefaultExplorerChartsPanelTitle(jobIds); + const jobs = await anomalyDetectorService.getJobs$(jobIds).toPromise(); + const influencers = anomalyDetectorService.extractInfluencers(jobs); + influencers.push(VIEW_BY_JOB_LABEL); + const modalSession = overlays.openModal( + toMountPoint( + { + modalSession.close(); + resolve({ + jobIds, + title: panelTitle, + maxSeriesToPlot, + }); + }} + onCancel={() => { + modalSession.close(); + reject(); + }} + /> + ) + ); + } catch (error) { + reject(error); + } }); } diff --git a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_setup_flyout.tsx b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_setup_flyout.tsx index e183907def57b..5027eb6783a64 100644 --- a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_setup_flyout.tsx +++ b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_setup_flyout.tsx @@ -25,31 +25,36 @@ export async function resolveAnomalySwimlaneUserInput( const anomalyDetectorService = new AnomalyDetectorService(new HttpService(http)); return new Promise(async (resolve, reject) => { - const { jobIds } = await resolveJobSelection(coreStart, input?.jobIds); - - const title = input?.title ?? getDefaultSwimlanePanelTitle(jobIds); - - const jobs = await anomalyDetectorService.getJobs$(jobIds).toPromise(); - - const influencers = anomalyDetectorService.extractInfluencers(jobs); - influencers.push(VIEW_BY_JOB_LABEL); - - const modalSession = overlays.openModal( - toMountPoint( - { - modalSession.close(); - resolve({ jobIds, title: panelTitle, swimlaneType, viewBy }); - }} - onCancel={() => { - modalSession.close(); - reject(); - }} - /> - ) - ); + try { + const { jobIds } = await resolveJobSelection(coreStart, input?.jobIds); + const title = input?.title ?? getDefaultSwimlanePanelTitle(jobIds); + const jobs = await anomalyDetectorService.getJobs$(jobIds).toPromise(); + const influencers = anomalyDetectorService.extractInfluencers(jobs); + influencers.push(VIEW_BY_JOB_LABEL); + const modalSession = overlays.openModal( + toMountPoint( + { + modalSession.close(); + resolve({ + jobIds, + title: panelTitle, + swimlaneType, + viewBy, + }); + }} + onCancel={() => { + modalSession.close(); + reject(); + }} + /> + ) + ); + } catch (error) { + reject(error); + } }); } diff --git a/x-pack/plugins/ml/public/embeddables/common/resolve_job_selection.tsx b/x-pack/plugins/ml/public/embeddables/common/resolve_job_selection.tsx index 1833883447859..fbceeb7f7cf79 100644 --- a/x-pack/plugins/ml/public/embeddables/common/resolve_job_selection.tsx +++ b/x-pack/plugins/ml/public/embeddables/common/resolve_job_selection.tsx @@ -38,56 +38,65 @@ export async function resolveJobSelection( } = coreStart; return new Promise(async (resolve, reject) => { - const maps = { - groupsMap: getInitialGroupsMap([]), - jobsMap: {}, - }; + try { + const maps = { + groupsMap: getInitialGroupsMap([]), + jobsMap: {}, + }; + const tzConfig = uiSettings.get('dateFormat:tz'); + const dateFormatTz = tzConfig !== 'Browser' ? tzConfig : moment.tz.guess(); - const tzConfig = uiSettings.get('dateFormat:tz'); - const dateFormatTz = tzConfig !== 'Browser' ? tzConfig : moment.tz.guess(); + const onFlyoutClose = () => { + flyoutSession.close(); + reject(); + }; - const onFlyoutClose = () => { - flyoutSession.close(); - reject(); - }; + const onSelectionConfirmed = async ({ + jobIds, + groups, + }: { + jobIds: string[]; + groups: Array<{ + groupId: string; + jobIds: string[]; + }>; + }) => { + await flyoutSession.close(); + resolve({ + jobIds, + groups, + }); + }; - const onSelectionConfirmed = async ({ - jobIds, - groups, - }: { - jobIds: string[]; - groups: Array<{ groupId: string; jobIds: string[] }>; - }) => { - await flyoutSession.close(); - resolve({ jobIds, groups }); - }; - const flyoutSession = coreStart.overlays.openFlyout( - toMountPoint( - - - - ), - { - 'data-test-subj': 'mlFlyoutJobSelector', - ownFocus: true, - closeButtonAriaLabel: 'jobSelectorFlyout', - } - ); + const flyoutSession = coreStart.overlays.openFlyout( + toMountPoint( + + + + ), + { + 'data-test-subj': 'mlFlyoutJobSelector', + ownFocus: true, + closeButtonAriaLabel: 'jobSelectorFlyout', + } + ); // Close the flyout when user navigates out of the dashboard plugin - // Close the flyout when user navigates out of the dashboard plugin - currentAppId$.pipe(takeUntil(from(flyoutSession.onClose))).subscribe((appId) => { - if (appId !== DashboardConstants.DASHBOARDS_ID) { - flyoutSession.close(); - } - }); + currentAppId$.pipe(takeUntil(from(flyoutSession.onClose))).subscribe((appId) => { + if (appId !== DashboardConstants.DASHBOARDS_ID) { + flyoutSession.close(); + } + }); + } catch (error) { + reject(error); + } }); } diff --git a/x-pack/plugins/monitoring/server/lib/logstash/get_paginated_pipelines.js b/x-pack/plugins/monitoring/server/lib/logstash/get_paginated_pipelines.js index 32662ae0efa34..a4645edda73d0 100644 --- a/x-pack/plugins/monitoring/server/lib/logstash/get_paginated_pipelines.js +++ b/x-pack/plugins/monitoring/server/lib/logstash/get_paginated_pipelines.js @@ -98,27 +98,39 @@ async function getPaginatedThroughputData(pipelines, req, lsIndexPattern, throug const metricSeriesData = Object.values( await Promise.all( pipelines.map((pipeline) => { - return new Promise(async (resolve) => { - const data = await getMetrics( - req, - lsIndexPattern, - [throughputMetric], - [ - { - bool: { - should: [ - { term: { type: 'logstash_stats' } }, - { term: { 'metricset.name': 'stats' } }, - ], + return new Promise(async (resolve, reject) => { + try { + const data = await getMetrics( + req, + lsIndexPattern, + [throughputMetric], + [ + { + bool: { + should: [ + { + term: { + type: 'logstash_stats', + }, + }, + { + term: { + 'metricset.name': 'stats', + }, + }, + ], + }, }, + ], + { + pipeline, }, - ], - { - pipeline, - }, - 2 - ); - resolve(reduceData(pipeline, data)); + 2 + ); + resolve(reduceData(pipeline, data)); + } catch (error) { + reject(error); + } }); }) ) @@ -184,27 +196,38 @@ async function getPipelines(req, lsIndexPattern, pipelines, throughputMetric, no async function getThroughputPipelines(req, lsIndexPattern, pipelines, throughputMetric) { const metricsResponse = await Promise.all( pipelines.map((pipeline) => { - return new Promise(async (resolve) => { - const data = await getMetrics( - req, - lsIndexPattern, - [throughputMetric], - [ - { - bool: { - should: [ - { term: { type: 'logstash_stats' } }, - { term: { 'metricset.name': 'stats' } }, - ], + return new Promise(async (resolve, reject) => { + try { + const data = await getMetrics( + req, + lsIndexPattern, + [throughputMetric], + [ + { + bool: { + should: [ + { + term: { + type: 'logstash_stats', + }, + }, + { + term: { + 'metricset.name': 'stats', + }, + }, + ], + }, }, - }, - ], - { - pipeline, - } - ); - - resolve(reduceData(pipeline, data)); + ], + { + pipeline, + } + ); + resolve(reduceData(pipeline, data)); + } catch (error) { + reject(error); + } }); }) ); diff --git a/x-pack/plugins/security/server/session_management/session_index.ts b/x-pack/plugins/security/server/session_management/session_index.ts index 9093d5d2e0db2..f1a9296177d9c 100644 --- a/x-pack/plugins/security/server/session_management/session_index.ts +++ b/x-pack/plugins/security/server/session_management/session_index.ts @@ -317,69 +317,73 @@ export class SessionIndex { const sessionIndexTemplateName = `${this.options.kibanaIndexName}_security_session_index_template_${SESSION_INDEX_TEMPLATE_VERSION}`; return (this.indexInitialization = new Promise(async (resolve, reject) => { - // Check if required index template exists. - let indexTemplateExists = false; try { - indexTemplateExists = ( - await this.options.elasticsearchClient.indices.existsTemplate({ - name: sessionIndexTemplateName, - }) - ).body; - } catch (err) { - this.options.logger.error( - `Failed to check if session index template exists: ${err.message}` - ); - return reject(err); - } - - // Create index template if it doesn't exist. - if (indexTemplateExists) { - this.options.logger.debug('Session index template already exists.'); - } else { + // Check if required index template exists. + let indexTemplateExists = false; try { - await this.options.elasticsearchClient.indices.putTemplate({ - name: sessionIndexTemplateName, - body: getSessionIndexTemplate(this.indexName), - }); - this.options.logger.debug('Successfully created session index template.'); + indexTemplateExists = ( + await this.options.elasticsearchClient.indices.existsTemplate({ + name: sessionIndexTemplateName, + }) + ).body; } catch (err) { - this.options.logger.error(`Failed to create session index template: ${err.message}`); + this.options.logger.error( + `Failed to check if session index template exists: ${err.message}` + ); return reject(err); } - } - // Check if required index exists. We cannot be sure that automatic creation of indices is - // always enabled, so we create session index explicitly. - let indexExists = false; - try { - indexExists = ( - await this.options.elasticsearchClient.indices.exists({ index: this.indexName }) - ).body; - } catch (err) { - this.options.logger.error(`Failed to check if session index exists: ${err.message}`); - return reject(err); - } + // Create index template if it doesn't exist. + if (indexTemplateExists) { + this.options.logger.debug('Session index template already exists.'); + } else { + try { + await this.options.elasticsearchClient.indices.putTemplate({ + name: sessionIndexTemplateName, + body: getSessionIndexTemplate(this.indexName), + }); + this.options.logger.debug('Successfully created session index template.'); + } catch (err) { + this.options.logger.error(`Failed to create session index template: ${err.message}`); + return reject(err); + } + } - // Create index if it doesn't exist. - if (indexExists) { - this.options.logger.debug('Session index already exists.'); - } else { + // Check if required index exists. We cannot be sure that automatic creation of indices is + // always enabled, so we create session index explicitly. + let indexExists = false; try { - await this.options.elasticsearchClient.indices.create({ index: this.indexName }); - this.options.logger.debug('Successfully created session index.'); + indexExists = ( + await this.options.elasticsearchClient.indices.exists({ index: this.indexName }) + ).body; } catch (err) { - // There can be a race condition if index is created by another Kibana instance. - if (err?.body?.error?.type === 'resource_already_exists_exception') { - this.options.logger.debug('Session index already exists.'); - } else { - this.options.logger.error(`Failed to create session index: ${err.message}`); - return reject(err); + this.options.logger.error(`Failed to check if session index exists: ${err.message}`); + return reject(err); + } + + // Create index if it doesn't exist. + if (indexExists) { + this.options.logger.debug('Session index already exists.'); + } else { + try { + await this.options.elasticsearchClient.indices.create({ index: this.indexName }); + this.options.logger.debug('Successfully created session index.'); + } catch (err) { + // There can be a race condition if index is created by another Kibana instance. + if (err?.body?.error?.type === 'resource_already_exists_exception') { + this.options.logger.debug('Session index already exists.'); + } else { + this.options.logger.error(`Failed to create session index: ${err.message}`); + return reject(err); + } } } - } - // Notify any consumers that are awaiting on this promise and immediately reset it. - resolve(); + // Notify any consumers that are awaiting on this promise and immediately reset it. + resolve(); + } catch (error) { + reject(error); + } }).finally(() => { this.indexInitialization = undefined; })); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/import_rules_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/import_rules_route.ts index 53bebf340c267..d3193900859fa 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/import_rules_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/import_rules_route.ts @@ -128,208 +128,225 @@ export const importRulesRoute = ( const batchParseObjects = chunkParseObjects.shift() ?? []; const newImportRuleResponse = await Promise.all( batchParseObjects.reduce>>((accum, parsedRule) => { - const importsWorkerPromise = new Promise(async (resolve) => { - if (parsedRule instanceof Error) { - // If the JSON object had a validation or parse error then we return - // early with the error and an (unknown) for the ruleId - resolve( - createBulkErrorObject({ - statusCode: 400, - message: parsedRule.message, - }) - ); - return null; - } - const { - anomaly_threshold: anomalyThreshold, - author, - building_block_type: buildingBlockType, - description, - enabled, - event_category_override: eventCategoryOverride, - false_positives: falsePositives, - from, - immutable, - query: queryOrUndefined, - language: languageOrUndefined, - license, - machine_learning_job_id: machineLearningJobId, - output_index: outputIndex, - saved_id: savedId, - meta, - filters: filtersRest, - rule_id: ruleId, - index, - interval, - max_signals: maxSignals, - risk_score: riskScore, - risk_score_mapping: riskScoreMapping, - rule_name_override: ruleNameOverride, - name, - severity, - severity_mapping: severityMapping, - tags, - threat, - threat_filters: threatFilters, - threat_index: threatIndex, - threat_query: threatQuery, - threat_mapping: threatMapping, - threat_language: threatLanguage, - threat_indicator_path: threatIndicatorPath, - concurrent_searches: concurrentSearches, - items_per_search: itemsPerSearch, - threshold, - timestamp_override: timestampOverride, - to, - type, - references, - note, - timeline_id: timelineId, - timeline_title: timelineTitle, - throttle, - version, - exceptions_list: exceptionsList, - } = parsedRule; - - try { - const query = !isMlRule(type) && queryOrUndefined == null ? '' : queryOrUndefined; - - const language = - !isMlRule(type) && languageOrUndefined == null ? 'kuery' : languageOrUndefined; - - // TODO: Fix these either with an is conversion or by better typing them within io-ts - const filters: PartialFilter[] | undefined = filtersRest as PartialFilter[]; + const importsWorkerPromise = new Promise( + async (resolve, reject) => { + try { + if (parsedRule instanceof Error) { + // If the JSON object had a validation or parse error then we return + // early with the error and an (unknown) for the ruleId + resolve( + createBulkErrorObject({ + statusCode: 400, + message: parsedRule.message, + }) + ); + return null; + } - throwHttpError(await mlAuthz.validateRuleType(type)); - - const rule = await readRules({ rulesClient, ruleId, id: undefined }); - if (rule == null) { - await createRules({ - rulesClient, - anomalyThreshold, + const { + anomaly_threshold: anomalyThreshold, author, - buildingBlockType, + building_block_type: buildingBlockType, description, enabled, - eventCategoryOverride, - falsePositives, + event_category_override: eventCategoryOverride, + false_positives: falsePositives, from, immutable, - query, - language, + query: queryOrUndefined, + language: languageOrUndefined, license, - machineLearningJobId, - outputIndex: signalsIndex, - savedId, - timelineId, - timelineTitle, + machine_learning_job_id: machineLearningJobId, + output_index: outputIndex, + saved_id: savedId, meta, - filters, - ruleId, + filters: filtersRest, + rule_id: ruleId, index, interval, - maxSignals, + max_signals: maxSignals, + risk_score: riskScore, + risk_score_mapping: riskScoreMapping, + rule_name_override: ruleNameOverride, name, - riskScore, - riskScoreMapping, - ruleNameOverride, severity, - severityMapping, + severity_mapping: severityMapping, tags, - throttle, - to, - type, threat, + threat_filters: threatFilters, + threat_index: threatIndex, + threat_query: threatQuery, + threat_mapping: threatMapping, + threat_language: threatLanguage, + threat_indicator_path: threatIndicatorPath, + concurrent_searches: concurrentSearches, + items_per_search: itemsPerSearch, threshold, - threatFilters, - threatIndex, - threatIndicatorPath, - threatQuery, - threatMapping, - threatLanguage, - concurrentSearches, - itemsPerSearch, - timestampOverride, - references, - note, - version, - exceptionsList, - actions: [], // Actions are not imported nor exported at this time - }); - resolve({ rule_id: ruleId, status_code: 200 }); - } else if (rule != null && request.query.overwrite) { - await patchRules({ - rulesClient, - author, - buildingBlockType, - spaceId: context.securitySolution.getSpaceId(), - ruleStatusClient, - description, - enabled, - eventCategoryOverride, - falsePositives, - from, - query, - language, - license, - outputIndex, - savedId, - timelineId, - timelineTitle, - meta, - filters, - rule, - index, - interval, - maxSignals, - riskScore, - riskScoreMapping, - ruleNameOverride, - name, - severity, - severityMapping, - tags, - timestampOverride, - throttle, + timestamp_override: timestampOverride, to, type, - threat, - threshold, - threatFilters, - threatIndex, - threatQuery, - threatMapping, - threatLanguage, - concurrentSearches, - itemsPerSearch, references, note, + timeline_id: timelineId, + timeline_title: timelineTitle, + throttle, version, - exceptionsList, - anomalyThreshold, - machineLearningJobId, - actions: undefined, - }); - resolve({ rule_id: ruleId, status_code: 200 }); - } else if (rule != null) { - resolve( - createBulkErrorObject({ + exceptions_list: exceptionsList, + } = parsedRule; + + try { + const query = + !isMlRule(type) && queryOrUndefined == null ? '' : queryOrUndefined; + const language = + !isMlRule(type) && languageOrUndefined == null + ? 'kuery' + : languageOrUndefined; // TODO: Fix these either with an is conversion or by better typing them within io-ts + + const filters: PartialFilter[] | undefined = filtersRest as PartialFilter[]; + throwHttpError(await mlAuthz.validateRuleType(type)); + const rule = await readRules({ + rulesClient, ruleId, - statusCode: 409, - message: `rule_id: "${ruleId}" already exists`, - }) - ); + id: undefined, + }); + + if (rule == null) { + await createRules({ + rulesClient, + anomalyThreshold, + author, + buildingBlockType, + description, + enabled, + eventCategoryOverride, + falsePositives, + from, + immutable, + query, + language, + license, + machineLearningJobId, + outputIndex: signalsIndex, + savedId, + timelineId, + timelineTitle, + meta, + filters, + ruleId, + index, + interval, + maxSignals, + name, + riskScore, + riskScoreMapping, + ruleNameOverride, + severity, + severityMapping, + tags, + throttle, + to, + type, + threat, + threshold, + threatFilters, + threatIndex, + threatIndicatorPath, + threatQuery, + threatMapping, + threatLanguage, + concurrentSearches, + itemsPerSearch, + timestampOverride, + references, + note, + version, + exceptionsList, + actions: [], // Actions are not imported nor exported at this time + }); + resolve({ + rule_id: ruleId, + status_code: 200, + }); + } else if (rule != null && request.query.overwrite) { + await patchRules({ + rulesClient, + author, + buildingBlockType, + spaceId: context.securitySolution.getSpaceId(), + ruleStatusClient, + description, + enabled, + eventCategoryOverride, + falsePositives, + from, + query, + language, + license, + outputIndex, + savedId, + timelineId, + timelineTitle, + meta, + filters, + rule, + index, + interval, + maxSignals, + riskScore, + riskScoreMapping, + ruleNameOverride, + name, + severity, + severityMapping, + tags, + timestampOverride, + throttle, + to, + type, + threat, + threshold, + threatFilters, + threatIndex, + threatQuery, + threatMapping, + threatLanguage, + concurrentSearches, + itemsPerSearch, + references, + note, + version, + exceptionsList, + anomalyThreshold, + machineLearningJobId, + actions: undefined, + }); + resolve({ + rule_id: ruleId, + status_code: 200, + }); + } else if (rule != null) { + resolve( + createBulkErrorObject({ + ruleId, + statusCode: 409, + message: `rule_id: "${ruleId}" already exists`, + }) + ); + } + } catch (err) { + resolve( + createBulkErrorObject({ + ruleId, + statusCode: err.statusCode ?? 400, + message: err.message, + }) + ); + } + } catch (error) { + reject(error); } - } catch (err) { - resolve( - createBulkErrorObject({ - ruleId, - statusCode: err.statusCode ?? 400, - message: err.message, - }) - ); } - }); + ); return [...accum, importsWorkerPromise]; }, []) ); diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/import_timelines/helpers.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/import_timelines/helpers.ts index 70d93d7552b1c..7e35c2163df70 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/import_timelines/helpers.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/import_timelines/helpers.ts @@ -106,133 +106,132 @@ export const importTimelines = async ( batchParseObjects.reduce>>((accum, parsedTimeline) => { const importsWorkerPromise = new Promise( async (resolve, reject) => { - if (parsedTimeline instanceof Error) { - // If the JSON object had a validation or parse error then we return - // early with the error and an (unknown) for the ruleId - resolve( - createBulkErrorObject({ - statusCode: 400, - message: parsedTimeline.message, - }) - ); - - return null; - } - - const { - savedObjectId, - pinnedEventIds, - globalNotes, - eventNotes, - status, - templateTimelineId, - templateTimelineVersion, - title, - timelineType, - version, - } = parsedTimeline; - - const parsedTimelineObject = omit(timelineSavedObjectOmittedFields, parsedTimeline); - let newTimeline = null; try { - const compareTimelinesStatus = new CompareTimelinesStatus({ + if (parsedTimeline instanceof Error) { + // If the JSON object had a validation or parse error then we return + // early with the error and an (unknown) for the ruleId + resolve( + createBulkErrorObject({ + statusCode: 400, + message: parsedTimeline.message, + }) + ); + return null; + } + + const { + savedObjectId, + pinnedEventIds, + globalNotes, + eventNotes, status, - timelineType, + templateTimelineId, + templateTimelineVersion, title, - timelineInput: { - id: savedObjectId, - version, - }, - templateTimelineInput: { - id: templateTimelineId, - version: templateTimelineVersion, - }, - frameworkRequest, - }); - await compareTimelinesStatus.init(); - const isTemplateTimeline = compareTimelinesStatus.isHandlingTemplateTimeline; - if (compareTimelinesStatus.isCreatableViaImport) { - // create timeline / timeline template - newTimeline = await createTimelines({ - frameworkRequest, - timeline: setTimeline(parsedTimelineObject, parsedTimeline, isTemplateTimeline), - pinnedEventIds: isTemplateTimeline ? null : pinnedEventIds, - notes: isTemplateTimeline ? globalNotes : [...globalNotes, ...eventNotes], - isImmutable, - overrideNotesOwner: false, - }); + timelineType, + version, + } = parsedTimeline; + const parsedTimelineObject = omit(timelineSavedObjectOmittedFields, parsedTimeline); + let newTimeline = null; - resolve({ - timeline_id: newTimeline.timeline.savedObjectId, - status_code: 200, - action: TimelineStatusActions.createViaImport, + try { + const compareTimelinesStatus = new CompareTimelinesStatus({ + status, + timelineType, + title, + timelineInput: { + id: savedObjectId, + version, + }, + templateTimelineInput: { + id: templateTimelineId, + version: templateTimelineVersion, + }, + frameworkRequest, }); - } + await compareTimelinesStatus.init(); + const isTemplateTimeline = compareTimelinesStatus.isHandlingTemplateTimeline; - if (!compareTimelinesStatus.isHandlingTemplateTimeline) { - const errorMessage = compareTimelinesStatus.checkIsFailureCases( - TimelineStatusActions.createViaImport - ); - const message = errorMessage?.body ?? DEFAULT_ERROR; - - resolve( - createBulkErrorObject({ - id: savedObjectId ?? 'unknown', - statusCode: 409, - message, - }) - ); - } else { - if (compareTimelinesStatus.isUpdatableViaImport) { - // update timeline template + if (compareTimelinesStatus.isCreatableViaImport) { + // create timeline / timeline template newTimeline = await createTimelines({ frameworkRequest, - timeline: parsedTimelineObject, - timelineSavedObjectId: compareTimelinesStatus.timelineId, - timelineVersion: compareTimelinesStatus.timelineVersion, - notes: globalNotes, - existingNoteIds: compareTimelinesStatus.timelineInput.data?.noteIds, + timeline: setTimeline(parsedTimelineObject, parsedTimeline, isTemplateTimeline), + pinnedEventIds: isTemplateTimeline ? null : pinnedEventIds, + notes: isTemplateTimeline ? globalNotes : [...globalNotes, ...eventNotes], isImmutable, overrideNotesOwner: false, }); - resolve({ timeline_id: newTimeline.timeline.savedObjectId, status_code: 200, - action: TimelineStatusActions.updateViaImport, + action: TimelineStatusActions.createViaImport, }); - } else { + } + + if (!compareTimelinesStatus.isHandlingTemplateTimeline) { const errorMessage = compareTimelinesStatus.checkIsFailureCases( - TimelineStatusActions.updateViaImport + TimelineStatusActions.createViaImport ); - const message = errorMessage?.body ?? DEFAULT_ERROR; - resolve( createBulkErrorObject({ - id: - savedObjectId ?? - (templateTimelineId - ? `(template_timeline_id) ${templateTimelineId}` - : 'unknown'), + id: savedObjectId ?? 'unknown', statusCode: 409, message, }) ); + } else { + if (compareTimelinesStatus.isUpdatableViaImport) { + // update timeline template + newTimeline = await createTimelines({ + frameworkRequest, + timeline: parsedTimelineObject, + timelineSavedObjectId: compareTimelinesStatus.timelineId, + timelineVersion: compareTimelinesStatus.timelineVersion, + notes: globalNotes, + existingNoteIds: compareTimelinesStatus.timelineInput.data?.noteIds, + isImmutable, + overrideNotesOwner: false, + }); + resolve({ + timeline_id: newTimeline.timeline.savedObjectId, + status_code: 200, + action: TimelineStatusActions.updateViaImport, + }); + } else { + const errorMessage = compareTimelinesStatus.checkIsFailureCases( + TimelineStatusActions.updateViaImport + ); + const message = errorMessage?.body ?? DEFAULT_ERROR; + resolve( + createBulkErrorObject({ + id: + savedObjectId ?? + (templateTimelineId + ? `(template_timeline_id) ${templateTimelineId}` + : 'unknown'), + statusCode: 409, + message, + }) + ); + } } + } catch (err) { + resolve( + createBulkErrorObject({ + id: + savedObjectId ?? + (templateTimelineId + ? `(template_timeline_id) ${templateTimelineId}` + : 'unknown'), + statusCode: 400, + message: err.message, + }) + ); } - } catch (err) { - resolve( - createBulkErrorObject({ - id: - savedObjectId ?? - (templateTimelineId - ? `(template_timeline_id) ${templateTimelineId}` - : 'unknown'), - statusCode: 400, - message: err.message, - }) - ); + } catch (error) { + reject(error); } } ); diff --git a/x-pack/plugins/task_manager/server/monitoring/configuration_statistics.test.ts b/x-pack/plugins/task_manager/server/monitoring/configuration_statistics.test.ts index 82a111305927f..e477edf3b9aed 100644 --- a/x-pack/plugins/task_manager/server/monitoring/configuration_statistics.test.ts +++ b/x-pack/plugins/task_manager/server/monitoring/configuration_statistics.test.ts @@ -47,62 +47,62 @@ describe('Configuration Statistics Aggregator', () => { }; return new Promise(async (resolve, reject) => { - createConfigurationAggregator(configuration, managedConfig) - .pipe(take(3), bufferCount(3)) - .subscribe(([initial, updatedWorkers, updatedInterval]) => { - expect(initial.value).toEqual({ - max_workers: 10, - poll_interval: 6000000, - max_poll_inactivity_cycles: 10, - request_capacity: 1000, - monitored_aggregated_stats_refresh_rate: 5000, - monitored_stats_running_average_window: 50, - monitored_task_execution_thresholds: { - default: { - error_threshold: 90, - warn_threshold: 80, + try { + createConfigurationAggregator(configuration, managedConfig) + .pipe(take(3), bufferCount(3)) + .subscribe(([initial, updatedWorkers, updatedInterval]) => { + expect(initial.value).toEqual({ + max_workers: 10, + poll_interval: 6000000, + max_poll_inactivity_cycles: 10, + request_capacity: 1000, + monitored_aggregated_stats_refresh_rate: 5000, + monitored_stats_running_average_window: 50, + monitored_task_execution_thresholds: { + default: { + error_threshold: 90, + warn_threshold: 80, + }, + custom: {}, }, - custom: {}, - }, - }); - - expect(updatedWorkers.value).toEqual({ - max_workers: 8, - poll_interval: 6000000, - max_poll_inactivity_cycles: 10, - request_capacity: 1000, - monitored_aggregated_stats_refresh_rate: 5000, - monitored_stats_running_average_window: 50, - monitored_task_execution_thresholds: { - default: { - error_threshold: 90, - warn_threshold: 80, + }); + expect(updatedWorkers.value).toEqual({ + max_workers: 8, + poll_interval: 6000000, + max_poll_inactivity_cycles: 10, + request_capacity: 1000, + monitored_aggregated_stats_refresh_rate: 5000, + monitored_stats_running_average_window: 50, + monitored_task_execution_thresholds: { + default: { + error_threshold: 90, + warn_threshold: 80, + }, + custom: {}, }, - custom: {}, - }, - }); - - expect(updatedInterval.value).toEqual({ - max_workers: 8, - poll_interval: 3000, - max_poll_inactivity_cycles: 10, - request_capacity: 1000, - monitored_aggregated_stats_refresh_rate: 5000, - monitored_stats_running_average_window: 50, - monitored_task_execution_thresholds: { - default: { - error_threshold: 90, - warn_threshold: 80, + }); + expect(updatedInterval.value).toEqual({ + max_workers: 8, + poll_interval: 3000, + max_poll_inactivity_cycles: 10, + request_capacity: 1000, + monitored_aggregated_stats_refresh_rate: 5000, + monitored_stats_running_average_window: 50, + monitored_task_execution_thresholds: { + default: { + error_threshold: 90, + warn_threshold: 80, + }, + custom: {}, }, - custom: {}, - }, - }); - resolve(); - }, reject); - - managedConfig.maxWorkersConfiguration$.next(8); - - managedConfig.pollIntervalConfiguration$.next(3000); + }); + resolve(); + }, reject); + managedConfig.maxWorkersConfiguration$.next(8); + managedConfig.pollIntervalConfiguration$.next(3000); + } catch (error) { + reject(error); + } }); }); }); diff --git a/x-pack/plugins/task_manager/server/monitoring/workload_statistics.test.ts b/x-pack/plugins/task_manager/server/monitoring/workload_statistics.test.ts index 9125bca8f5b05..d24931646128a 100644 --- a/x-pack/plugins/task_manager/server/monitoring/workload_statistics.test.ts +++ b/x-pack/plugins/task_manager/server/monitoring/workload_statistics.test.ts @@ -328,27 +328,44 @@ describe('Workload Statistics Aggregator', () => { loggingSystemMock.create().get() ); - return new Promise(async (resolve) => { - workloadAggregator.pipe(first()).subscribe((result) => { - expect(result.key).toEqual('workload'); - expect(result.value).toMatchObject({ - count: 4, - task_types: { - actions_telemetry: { count: 2, status: { idle: 2 } }, - alerting_telemetry: { count: 1, status: { idle: 1 } }, - session_cleanup: { count: 1, status: { idle: 1 } }, - }, + return new Promise(async (resolve, reject) => { + try { + workloadAggregator.pipe(first()).subscribe((result) => { + expect(result.key).toEqual('workload'); + expect(result.value).toMatchObject({ + count: 4, + task_types: { + actions_telemetry: { + count: 2, + status: { + idle: 2, + }, + }, + alerting_telemetry: { + count: 1, + status: { + idle: 1, + }, + }, + session_cleanup: { + count: 1, + status: { + idle: 1, + }, + }, + }, + }); + resolve(); }); - resolve(); - }); - - availability$.next(false); - - await sleep(10); - expect(taskStore.aggregate).not.toHaveBeenCalled(); - await sleep(10); - expect(taskStore.aggregate).not.toHaveBeenCalled(); - availability$.next(true); + availability$.next(false); + await sleep(10); + expect(taskStore.aggregate).not.toHaveBeenCalled(); + await sleep(10); + expect(taskStore.aggregate).not.toHaveBeenCalled(); + availability$.next(true); + } catch (error) { + reject(error); + } }); }); diff --git a/x-pack/plugins/task_manager/server/task_scheduling.ts b/x-pack/plugins/task_manager/server/task_scheduling.ts index 88176b25680ca..a89f66d9c772b 100644 --- a/x-pack/plugins/task_manager/server/task_scheduling.ts +++ b/x-pack/plugins/task_manager/server/task_scheduling.ts @@ -113,11 +113,18 @@ export class TaskScheduling { */ public async runNow(taskId: string): Promise { return new Promise(async (resolve, reject) => { - this.awaitTaskRunResult(taskId) - // don't expose state on runNow - .then(({ id }) => resolve({ id })) - .catch(reject); - this.taskPollingLifecycle.attemptToRun(taskId); + try { + this.awaitTaskRunResult(taskId) // don't expose state on runNow + .then(({ id }) => + resolve({ + id, + }) + ) + .catch(reject); + this.taskPollingLifecycle.attemptToRun(taskId); + } catch (error) { + reject(error); + } }); } @@ -137,39 +144,42 @@ export class TaskScheduling { taskInstance: task, }); return new Promise(async (resolve, reject) => { - // The actual promise returned from this function is resolved after the awaitTaskRunResult promise resolves. - // However, we do not wait to await this promise, as we want later execution to happen in parallel. - // The awaitTaskRunResult promise is resolved once the ephemeral task is successfully executed (technically, when a TaskEventType.TASK_RUN is emitted with the same id). - // However, the ephemeral task won't even get into the queue until the subsequent this.ephemeralTaskLifecycle.attemptToRun is called (which puts it in the queue). - - // The reason for all this confusion? Timing. - - // In the this.ephemeralTaskLifecycle.attemptToRun, it's possible that the ephemeral task is put into the queue and processed before this function call returns anything. - // If that happens, putting the awaitTaskRunResult after would just hang because the task already completed. We need to listen for the completion before we add it to the queue to avoid this possibility. - const { cancel, resolveOnCancel } = cancellablePromise(); - this.awaitTaskRunResult(id, resolveOnCancel) - .then((arg: RunNowResult) => { - resolve(arg); - }) - .catch((err: Error) => { - reject(err); + try { + // The actual promise returned from this function is resolved after the awaitTaskRunResult promise resolves. + // However, we do not wait to await this promise, as we want later execution to happen in parallel. + // The awaitTaskRunResult promise is resolved once the ephemeral task is successfully executed (technically, when a TaskEventType.TASK_RUN is emitted with the same id). + // However, the ephemeral task won't even get into the queue until the subsequent this.ephemeralTaskLifecycle.attemptToRun is called (which puts it in the queue). + // The reason for all this confusion? Timing. + // In the this.ephemeralTaskLifecycle.attemptToRun, it's possible that the ephemeral task is put into the queue and processed before this function call returns anything. + // If that happens, putting the awaitTaskRunResult after would just hang because the task already completed. We need to listen for the completion before we add it to the queue to avoid this possibility. + const { cancel, resolveOnCancel } = cancellablePromise(); + this.awaitTaskRunResult(id, resolveOnCancel) + .then((arg: RunNowResult) => { + resolve(arg); + }) + .catch((err: Error) => { + reject(err); + }); + const attemptToRunResult = this.ephemeralTaskLifecycle.attemptToRun({ + id, + scheduledAt: new Date(), + runAt: new Date(), + status: TaskStatus.Idle, + ownerId: this.taskManagerId, + ...modifiedTask, }); - const attemptToRunResult = this.ephemeralTaskLifecycle.attemptToRun({ - id, - scheduledAt: new Date(), - runAt: new Date(), - status: TaskStatus.Idle, - ownerId: this.taskManagerId, - ...modifiedTask, - }); - if (isErr(attemptToRunResult)) { - cancel(); - reject( - new EphemeralTaskRejectedDueToCapacityError( - `Ephemeral Task of type ${task.taskType} was rejected`, - task - ) - ); + + if (isErr(attemptToRunResult)) { + cancel(); + reject( + new EphemeralTaskRejectedDueToCapacityError( + `Ephemeral Task of type ${task.taskType} was rejected`, + task + ) + ); + } + } catch (error) { + reject(error); } }); } diff --git a/x-pack/test/functional_enterprise_search/services/app_search_client.ts b/x-pack/test/functional_enterprise_search/services/app_search_client.ts index 8e829b97e9dda..457523bccf8c3 100644 --- a/x-pack/test/functional_enterprise_search/services/app_search_client.ts +++ b/x-pack/test/functional_enterprise_search/services/app_search_client.ts @@ -105,14 +105,20 @@ const search = async (engineName: string): Promise => { // Since the App Search API does not issue document receipts, the only way to tell whether or not documents // are fully indexed is to poll the search endpoint. export const waitForIndexedDocs = (engineName: string) => { - return new Promise(async function (resolve) { - let isReady = false; - while (!isReady) { - const response = await search(engineName); - if (response.results && response.results.length > 0) { - isReady = true; - resolve(); + return new Promise(async function (resolve, reject) { + try { + let isReady = false; + + while (!isReady) { + const response = await search(engineName); + + if (response.results && response.results.length > 0) { + isReady = true; + resolve(); + } } + } catch (error) { + reject(error); } }); }; diff --git a/x-pack/test/lists_api_integration/utils.ts b/x-pack/test/lists_api_integration/utils.ts index 8975feb6fbe05..b3816ad7563b8 100644 --- a/x-pack/test/lists_api_integration/utils.ts +++ b/x-pack/test/lists_api_integration/utils.ts @@ -116,21 +116,29 @@ export const waitFor = async ( timeoutWait: number = 10 ) => { await new Promise(async (resolve, reject) => { - let found = false; - let numberOfTries = 0; - while (!found && numberOfTries < Math.floor(maxTimeout / timeoutWait)) { - const itPasses = await functionToTest(); - if (itPasses) { - found = true; + try { + let found = false; + let numberOfTries = 0; + + while (!found && numberOfTries < Math.floor(maxTimeout / timeoutWait)) { + const itPasses = await functionToTest(); + + if (itPasses) { + found = true; + } else { + numberOfTries++; + } + + await new Promise((resolveTimeout) => setTimeout(resolveTimeout, timeoutWait)); + } + + if (found) { + resolve(); } else { - numberOfTries++; + reject(new Error(`timed out waiting for function ${functionName} condition to be true`)); } - await new Promise((resolveTimeout) => setTimeout(resolveTimeout, timeoutWait)); - } - if (found) { - resolve(); - } else { - reject(new Error(`timed out waiting for function ${functionName} condition to be true`)); + } catch (error) { + reject(error); } }); }; diff --git a/yarn.lock b/yarn.lock index 4d1520537ef6d..80c66de7e3553 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6457,6 +6457,11 @@ resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-4.14.1.tgz#b3d2eb91dafd0fd8b3fce7c61512ac66bd0364aa" integrity sha512-SkhzHdI/AllAgQSxXM89XwS1Tkic7csPdndUuTKabEwRcEfR8uQ/iPA3Dgio1rqsV3jtqZhY0QQni8rLswJM2w== +"@typescript-eslint/types@4.28.3": + version "4.28.3" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-4.28.3.tgz#8fffd436a3bada422c2c1da56060a0566a9506c7" + integrity sha512-kQFaEsQBQVtA9VGVyciyTbIg7S3WoKHNuOp/UF5RG40900KtGqfoiETWD/v0lzRXc+euVE9NXmfer9dLkUJrkA== + "@typescript-eslint/types@4.3.0": version "4.3.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-4.3.0.tgz#1f0b2d5e140543e2614f06d48fb3ae95193c6ddf" @@ -6490,6 +6495,19 @@ semver "^7.3.2" tsutils "^3.17.1" +"@typescript-eslint/typescript-estree@^4.14.1": + version "4.28.3" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-4.28.3.tgz#253d7088100b2a38aefe3c8dd7bd1f8232ec46fb" + integrity sha512-YAb1JED41kJsqCQt1NcnX5ZdTA93vKFCMP4lQYG6CFxd0VzDJcKttRlMrlG+1qiWAw8+zowmHU1H0OzjWJzR2w== + dependencies: + "@typescript-eslint/types" "4.28.3" + "@typescript-eslint/visitor-keys" "4.28.3" + debug "^4.3.1" + globby "^11.0.3" + is-glob "^4.0.1" + semver "^7.3.5" + tsutils "^3.21.0" + "@typescript-eslint/visitor-keys@4.14.1": version "4.14.1" resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-4.14.1.tgz#e93c2ff27f47ee477a929b970ca89d60a117da91" @@ -6498,6 +6516,14 @@ "@typescript-eslint/types" "4.14.1" eslint-visitor-keys "^2.0.0" +"@typescript-eslint/visitor-keys@4.28.3": + version "4.28.3" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-4.28.3.tgz#26ac91e84b23529968361045829da80a4e5251c4" + integrity sha512-ri1OzcLnk1HH4gORmr1dllxDzzrN6goUIz/P4MHFV0YZJDCADPR3RvYNp0PW2SetKTThar6wlbFTL00hV2Q+fg== + dependencies: + "@typescript-eslint/types" "4.28.3" + eslint-visitor-keys "^2.0.0" + "@typescript-eslint/visitor-keys@4.3.0": version "4.3.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-4.3.0.tgz#0e5ab0a09552903edeae205982e8521e17635ae0" @@ -13120,6 +13146,11 @@ eslint-scope@^5.0.0: esrecurse "^4.1.0" estraverse "^4.1.1" +eslint-traverse@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/eslint-traverse/-/eslint-traverse-1.0.0.tgz#108d360a171a6e6334e1af0cee905a93bd0dcc53" + integrity sha512-bSp37rQs93LF8rZ409EI369DGCI4tELbFVmFNxI6QbuveS7VRxYVyUhwDafKN/enMyUh88HQQ7ZoGUHtPuGdcw== + eslint-utils@^1.4.3: version "1.4.3" resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-1.4.3.tgz#74fec7c54d0776b6f67e0251040b5806564e981f" @@ -27515,6 +27546,13 @@ tsutils@^3.17.1: dependencies: tslib "^1.8.1" +tsutils@^3.21.0: + version "3.21.0" + resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.21.0.tgz#b48717d394cea6c1e096983eed58e9d61715b623" + integrity sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA== + dependencies: + tslib "^1.8.1" + tty-browserify@0.0.0: version "0.0.0" resolved "https://registry.yarnpkg.com/tty-browserify/-/tty-browserify-0.0.0.tgz#a157ba402da24e9bf957f9aa69d524eed42901a6" From 634c272edddcba9aa259f216024c8a4dcc4a02d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cau=C3=AA=20Marcondes?= <55978943+cauemarcondes@users.noreply.github.com> Date: Tue, 31 Aug 2021 18:02:00 -0400 Subject: [PATCH 17/81] [APM] Fleet: adding support for legacy fields (#110136) * supporting legacy fields * addressing PR comments Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../get_apm_package_policy_definition.ts | 46 ++++++++++++++++++- 1 file changed, 45 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/apm/server/lib/fleet/get_apm_package_policy_definition.ts b/x-pack/plugins/apm/server/lib/fleet/get_apm_package_policy_definition.ts index b339c1f1f0be9..afe3a95d79023 100644 --- a/x-pack/plugins/apm/server/lib/fleet/get_apm_package_policy_definition.ts +++ b/x-pack/plugins/apm/server/lib/fleet/get_apm_package_policy_definition.ts @@ -16,6 +16,7 @@ interface GetApmPackagePolicyDefinitionOptions { export function getApmPackagePolicyDefinition( options: GetApmPackagePolicyDefinitionOptions ) { + const { apmServerSchema, cloudPluginSetup } = options; return { name: 'Elastic APM', namespace: 'default', @@ -27,7 +28,10 @@ export function getApmPackagePolicyDefinition( type: 'apm', enabled: true, streams: [], - vars: getApmPackageInputVars(options), + vars: getApmPackageInputVars({ + cloudPluginSetup, + apmServerSchema: preprocessLegacyFields({ apmServerSchema }), + }), }, ], package: { @@ -38,6 +42,34 @@ export function getApmPackagePolicyDefinition( }; } +function preprocessLegacyFields({ + apmServerSchema, +}: { + apmServerSchema: Record; +}) { + const copyOfApmServerSchema = { ...apmServerSchema }; + [ + { + key: 'apm-server.auth.anonymous.rate_limit.event_limit', + legacyKey: 'apm-server.rum.event_rate.limit', + }, + { + key: 'apm-server.auth.anonymous.rate_limit.ip_limit', + legacyKey: 'apm-server.rum.event_rate.lru_size', + }, + { + key: 'apm-server.auth.anonymous.allow_service', + legacyKey: 'apm-server.rum.allow_service_names', + }, + ].forEach(({ key, legacyKey }) => { + if (!copyOfApmServerSchema[key]) { + copyOfApmServerSchema[key] = copyOfApmServerSchema[legacyKey]; + delete copyOfApmServerSchema[legacyKey]; + } + }); + return copyOfApmServerSchema; +} + function getApmPackageInputVars(options: GetApmPackagePolicyDefinitionOptions) { const { apmServerSchema } = options; const apmServerConfigs = Object.entries( @@ -90,6 +122,18 @@ export const apmConfigMapping: Record< name: 'rum_allow_headers', type: 'text', }, + 'apm-server.rum.event_rate.limit': { + name: 'rum_event_rate_limit', + type: 'integer', + }, + 'apm-server.rum.allow_service_names': { + name: 'rum_allow_service_names', + type: 'text', + }, + 'apm-server.rum.event_rate.lru_size': { + name: 'rum_event_rate_lru_size', + type: 'integer', + }, 'apm-server.rum.response_headers': { name: 'rum_response_headers', type: 'yaml', From 40b91c97ce9f402a475aa0ffbe84b41be5b71d04 Mon Sep 17 00:00:00 2001 From: Jonathan Budzenski Date: Tue, 31 Aug 2021 18:00:29 -0500 Subject: [PATCH 18/81] [deb/rpm] Generate os package specific kibana.yml (#98213) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- src/dev/build/build_distributables.ts | 4 ++ .../create_os_package_kibana_yml.ts | 50 +++++++++++++++++++ .../os_packages/create_os_package_tasks.ts | 9 ++++ src/dev/build/tasks/os_packages/run_fpm.ts | 1 + .../usr/lib/systemd/system/kibana.service | 2 +- 5 files changed, 65 insertions(+), 1 deletion(-) create mode 100644 src/dev/build/tasks/os_packages/create_os_package_kibana_yml.ts diff --git a/src/dev/build/build_distributables.ts b/src/dev/build/build_distributables.ts index 9ddf02e101a19..1042cdc484c12 100644 --- a/src/dev/build/build_distributables.ts +++ b/src/dev/build/build_distributables.ts @@ -105,6 +105,10 @@ export async function buildDistributables(log: ToolingLog, options: BuildOptions // control w/ --skip-archives await run(Tasks.CreateArchives); } + + if (options.createDebPackage || options.createRpmPackage) { + await run(Tasks.CreatePackageConfig); + } if (options.createDebPackage) { // control w/ --deb or --skip-os-packages await run(Tasks.CreateDebPackage); diff --git a/src/dev/build/tasks/os_packages/create_os_package_kibana_yml.ts b/src/dev/build/tasks/os_packages/create_os_package_kibana_yml.ts new file mode 100644 index 0000000000000..e7137ada02182 --- /dev/null +++ b/src/dev/build/tasks/os_packages/create_os_package_kibana_yml.ts @@ -0,0 +1,50 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { readFileSync, writeFileSync } from 'fs'; +import { resolve } from 'path'; +import { Build, Config, mkdirp } from '../../lib'; + +export async function createOSPackageKibanaYML(config: Config, build: Build) { + const configReadPath = config.resolveFromRepo('config', 'kibana.yml'); + const configWriteDir = config.resolveFromRepo('build', 'os_packages', 'config'); + const configWritePath = resolve(configWriteDir, 'kibana.yml'); + + await mkdirp(configWriteDir); + + let kibanaYML = readFileSync(configReadPath, { encoding: 'utf8' }); + + [ + [/#pid.file:.*/g, 'pid.file: /run/kibana/kibana.pid'], + [/#logging.dest:.*/g, 'logging.dest: /var/log/kibana/kibana.log'], + ].forEach((options) => { + const [regex, setting] = options; + const diff = kibanaYML; + const match = kibanaYML.search(regex) >= 0; + if (match) { + if (typeof setting === 'string') { + kibanaYML = kibanaYML.replace(regex, setting); + } + } + + if (!diff.localeCompare(kibanaYML)) { + throw new Error( + `OS package configuration unmodified. Verify match for ${regex} is available` + ); + } + }); + + try { + writeFileSync(configWritePath, kibanaYML, { flag: 'wx' }); + } catch (err) { + if (err.code === 'EEXIST') { + return; + } + throw err; + } +} diff --git a/src/dev/build/tasks/os_packages/create_os_package_tasks.ts b/src/dev/build/tasks/os_packages/create_os_package_tasks.ts index 99d0e1998e78a..67a9e86ee2073 100644 --- a/src/dev/build/tasks/os_packages/create_os_package_tasks.ts +++ b/src/dev/build/tasks/os_packages/create_os_package_tasks.ts @@ -9,6 +9,15 @@ import { Task } from '../../lib'; import { runFpm } from './run_fpm'; import { runDockerGenerator } from './docker_generator'; +import { createOSPackageKibanaYML } from './create_os_package_kibana_yml'; + +export const CreatePackageConfig: Task = { + description: 'Creating OS package kibana.yml', + + async run(config, log, build) { + await createOSPackageKibanaYML(config, build); + }, +}; export const CreateDebPackage: Task = { description: 'Creating deb package', diff --git a/src/dev/build/tasks/os_packages/run_fpm.ts b/src/dev/build/tasks/os_packages/run_fpm.ts index b732e4c80ea37..c7d9f6997cdf2 100644 --- a/src/dev/build/tasks/os_packages/run_fpm.ts +++ b/src/dev/build/tasks/os_packages/run_fpm.ts @@ -123,6 +123,7 @@ export async function runFpm( `${resolveWithTrailingSlash(fromBuild('.'))}=/usr/share/kibana/`, // copy the config directory to /etc/kibana + `${config.resolveFromRepo('build/os_packages/config/kibana.yml')}=/etc/kibana/kibana.yml`, `${resolveWithTrailingSlash(fromBuild('config'))}=/etc/kibana/`, // copy the data directory at /var/lib/kibana diff --git a/src/dev/build/tasks/os_packages/service_templates/systemd/usr/lib/systemd/system/kibana.service b/src/dev/build/tasks/os_packages/service_templates/systemd/usr/lib/systemd/system/kibana.service index 7a1508d91b213..df33b82f1f967 100644 --- a/src/dev/build/tasks/os_packages/service_templates/systemd/usr/lib/systemd/system/kibana.service +++ b/src/dev/build/tasks/os_packages/service_templates/systemd/usr/lib/systemd/system/kibana.service @@ -15,7 +15,7 @@ Environment=KBN_PATH_CONF=/etc/kibana EnvironmentFile=-/etc/default/kibana EnvironmentFile=-/etc/sysconfig/kibana -ExecStart=/usr/share/kibana/bin/kibana --logging.dest="/var/log/kibana/kibana.log" --pid.file="/run/kibana/kibana.pid" +ExecStart=/usr/share/kibana/bin/kibana Restart=on-failure RestartSec=3 From 3e15695d06355867f88659ba0585795d0a1873ce Mon Sep 17 00:00:00 2001 From: Yuliia Naumenko Date: Tue, 31 Aug 2021 16:38:33 -0700 Subject: [PATCH 19/81] [Alerting][8.0] Prepare alerting SOs to sharecapable (#110386) * [Alerting] [8.0] Prepare for making alerting saved objects sharecapable (#109990) * [Alerting] [8.0] Prepare for making alerting saved objects sharecapable * removed v8 check * removed link * added no op migration * fixed name Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> * [Actions] [8.0] Prepare for making action saved objects sharecapable. (#109756) * [Actions] [8.0] Prepare for making action saved objects sharecapable. * added more tests * made it compatible to merge to 7.x * fixed due to comments * fixed tests * added tests * fixed tests * fixed due to comments * added no-opactions migration * fixed test * [Task Manager][8.0] Added migrations to savedObject Ids for "actions:* and "alerting:*" task types (#109180) * [Task Manager][8.0] Added migrations to savedObject Ids for "actions:* and "alerting:*" task types * fixed due to comments * fixed typo * added more tests * added unit test * added func test * added func tests * fixed test Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> * fixed merge * fixed legacy tests * fixed tests * fixed eslint * Update migrations.ts fixed action task * fixed due to comments Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../action_task_params_migrations.test.ts | 8 + .../action_task_params_migrations.ts | 9 + .../saved_objects/actions_migrations.test.ts | 8 + .../saved_objects/actions_migrations.ts | 8 + .../actions/server/saved_objects/index.ts | 6 +- .../alerting/server/saved_objects/index.ts | 3 +- .../server/saved_objects/migrations.test.ts | 8 + .../server/saved_objects/migrations.ts | 7 + .../server/saved_objects/index.ts | 4 +- .../server/saved_objects/migrations.test.ts | 169 +++++++++++++ .../server/saved_objects/migrations.ts | 128 +++++++++- .../tests/alerting/rbac_legacy.ts | 38 ++- .../spaces_only/tests/alerting/create.ts | 2 +- .../es_archives/task_manager_tasks/data.json | 61 +++++ .../task_manager_tasks/mappings.json | 225 ++++++++++++++++++ .../test_suites/task_manager/index.ts | 2 + .../test_suites/task_manager/migrations.ts | 70 ++++++ 17 files changed, 736 insertions(+), 20 deletions(-) create mode 100644 x-pack/plugins/task_manager/server/saved_objects/migrations.test.ts create mode 100644 x-pack/test/functional/es_archives/task_manager_tasks/data.json create mode 100644 x-pack/test/functional/es_archives/task_manager_tasks/mappings.json create mode 100644 x-pack/test/plugin_api_integration/test_suites/task_manager/migrations.ts diff --git a/x-pack/plugins/actions/server/saved_objects/action_task_params_migrations.test.ts b/x-pack/plugins/actions/server/saved_objects/action_task_params_migrations.test.ts index ceea9f3cff18f..6d7fd940612f3 100644 --- a/x-pack/plugins/actions/server/saved_objects/action_task_params_migrations.test.ts +++ b/x-pack/plugins/actions/server/saved_objects/action_task_params_migrations.test.ts @@ -356,6 +356,14 @@ describe('successful migrations', () => { }); }); }); + + describe('8.0.0', () => { + test('no op migration for rules SO', () => { + const migration800 = getActionTaskParamsMigrations(encryptedSavedObjectsSetup, [])['8.0.0']; + const actionTaskParam = getMockData(); + expect(migration800(actionTaskParam, context)).toEqual(actionTaskParam); + }); + }); }); describe('handles errors during migrations', () => { diff --git a/x-pack/plugins/actions/server/saved_objects/action_task_params_migrations.ts b/x-pack/plugins/actions/server/saved_objects/action_task_params_migrations.ts index 3612642160443..ceb82146a03eb 100644 --- a/x-pack/plugins/actions/server/saved_objects/action_task_params_migrations.ts +++ b/x-pack/plugins/actions/server/saved_objects/action_task_params_migrations.ts @@ -48,8 +48,17 @@ export function getActionTaskParamsMigrations( pipeMigrations(getUseSavedObjectReferencesFn(preconfiguredActions)) ); + const migrationActionsTaskParams800 = createEsoMigration( + encryptedSavedObjects, + ( + doc: SavedObjectUnsanitizedDoc + ): doc is SavedObjectUnsanitizedDoc => true, + (doc) => doc // no-op + ); + return { '7.16.0': executeMigrationWithErrorHandling(migrationActionTaskParamsSixteen, '7.16.0'), + '8.0.0': executeMigrationWithErrorHandling(migrationActionsTaskParams800, '8.0.0'), }; } diff --git a/x-pack/plugins/actions/server/saved_objects/actions_migrations.test.ts b/x-pack/plugins/actions/server/saved_objects/actions_migrations.test.ts index bc0e59279abc1..7dc1426c13a4b 100644 --- a/x-pack/plugins/actions/server/saved_objects/actions_migrations.test.ts +++ b/x-pack/plugins/actions/server/saved_objects/actions_migrations.test.ts @@ -118,6 +118,14 @@ describe('successful migrations', () => { }); }); }); + + describe('8.0.0', () => { + test('no op migration for rules SO', () => { + const migration800 = getActionsMigrations(encryptedSavedObjectsSetup)['8.0.0']; + const action = getMockData({}); + expect(migration800(action, context)).toEqual(action); + }); + }); }); describe('handles errors during migrations', () => { diff --git a/x-pack/plugins/actions/server/saved_objects/actions_migrations.ts b/x-pack/plugins/actions/server/saved_objects/actions_migrations.ts index a72565e00ef7b..7857a9e1f833f 100644 --- a/x-pack/plugins/actions/server/saved_objects/actions_migrations.ts +++ b/x-pack/plugins/actions/server/saved_objects/actions_migrations.ts @@ -62,10 +62,18 @@ export function getActionsMigrations( pipeMigrations(addisMissingSecretsField) ); + const migrationActions800 = createEsoMigration( + encryptedSavedObjects, + (doc: SavedObjectUnsanitizedDoc): doc is SavedObjectUnsanitizedDoc => + true, + (doc) => doc // no-op + ); + return { '7.10.0': executeMigrationWithErrorHandling(migrationActionsTen, '7.10.0'), '7.11.0': executeMigrationWithErrorHandling(migrationActionsEleven, '7.11.0'), '7.14.0': executeMigrationWithErrorHandling(migrationActionsFourteen, '7.14.0'), + '8.0.0': executeMigrationWithErrorHandling(migrationActions800, '8.0.0'), }; } diff --git a/x-pack/plugins/actions/server/saved_objects/index.ts b/x-pack/plugins/actions/server/saved_objects/index.ts index 71ec92645b249..14b425d20af13 100644 --- a/x-pack/plugins/actions/server/saved_objects/index.ts +++ b/x-pack/plugins/actions/server/saved_objects/index.ts @@ -35,7 +35,8 @@ export function setupSavedObjects( savedObjects.registerType({ name: ACTION_SAVED_OBJECT_TYPE, hidden: true, - namespaceType: 'single', + namespaceType: 'multiple-isolated', + convertToMultiNamespaceTypeVersion: '8.0.0', mappings: mappings.action as SavedObjectsTypeMappingDefinition, migrations: getActionsMigrations(encryptedSavedObjects), management: { @@ -71,7 +72,8 @@ export function setupSavedObjects( savedObjects.registerType({ name: ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE, hidden: true, - namespaceType: 'single', + namespaceType: 'multiple-isolated', + convertToMultiNamespaceTypeVersion: '8.0.0', mappings: mappings.action_task_params as SavedObjectsTypeMappingDefinition, migrations: getActionTaskParamsMigrations(encryptedSavedObjects, preconfiguredActions), excludeOnUpgrade: async ({ readonlyEsClient }) => { diff --git a/x-pack/plugins/alerting/server/saved_objects/index.ts b/x-pack/plugins/alerting/server/saved_objects/index.ts index b1d56a364a3dd..f1afba147a2f7 100644 --- a/x-pack/plugins/alerting/server/saved_objects/index.ts +++ b/x-pack/plugins/alerting/server/saved_objects/index.ts @@ -53,7 +53,8 @@ export function setupSavedObjects( savedObjects.registerType({ name: 'alert', hidden: true, - namespaceType: 'single', + namespaceType: 'multiple-isolated', + convertToMultiNamespaceTypeVersion: '8.0.0', migrations: getMigrations(encryptedSavedObjects, isPreconfigured), mappings: mappings.alert as SavedObjectsTypeMappingDefinition, management: { diff --git a/x-pack/plugins/alerting/server/saved_objects/migrations.test.ts b/x-pack/plugins/alerting/server/saved_objects/migrations.test.ts index e460167b40d23..5e850ad3226f8 100644 --- a/x-pack/plugins/alerting/server/saved_objects/migrations.test.ts +++ b/x-pack/plugins/alerting/server/saved_objects/migrations.test.ts @@ -1700,6 +1700,14 @@ describe('successful migrations', () => { }); }); }); + + describe('8.0.0', () => { + test('no op migration for rules SO', () => { + const migration800 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['8.0.0']; + const alert = getMockData({}, true); + expect(migration800(alert, migrationContext)).toEqual(alert); + }); + }); }); describe('handles errors during migrations', () => { diff --git a/x-pack/plugins/alerting/server/saved_objects/migrations.ts b/x-pack/plugins/alerting/server/saved_objects/migrations.ts index c0af554cd7a44..287636c69bb75 100644 --- a/x-pack/plugins/alerting/server/saved_objects/migrations.ts +++ b/x-pack/plugins/alerting/server/saved_objects/migrations.ts @@ -106,6 +106,12 @@ export function getMigrations( pipeMigrations(setLegacyId, getRemovePreconfiguredConnectorsFromReferencesFn(isPreconfigured)) ); + const migrationRules800 = createEsoMigration( + encryptedSavedObjects, + (doc: SavedObjectUnsanitizedDoc): doc is SavedObjectUnsanitizedDoc => true, + (doc) => doc // no-op + ); + return { '7.10.0': executeMigrationWithErrorHandling(migrationWhenRBACWasIntroduced, '7.10.0'), '7.11.0': executeMigrationWithErrorHandling(migrationAlertUpdatedAtAndNotifyWhen, '7.11.0'), @@ -114,6 +120,7 @@ export function getMigrations( '7.14.1': executeMigrationWithErrorHandling(migrationSecurityRules714, '7.14.1'), '7.15.0': executeMigrationWithErrorHandling(migrationSecurityRules715, '7.15.0'), '7.16.0': executeMigrationWithErrorHandling(migrateRules716, '7.16.0'), + '8.0.0': executeMigrationWithErrorHandling(migrationRules800, '8.0.0'), }; } diff --git a/x-pack/plugins/task_manager/server/saved_objects/index.ts b/x-pack/plugins/task_manager/server/saved_objects/index.ts index d2d079c7747b1..abbd1af73b55a 100644 --- a/x-pack/plugins/task_manager/server/saved_objects/index.ts +++ b/x-pack/plugins/task_manager/server/saved_objects/index.ts @@ -8,7 +8,7 @@ import type { SavedObjectsServiceSetup, SavedObjectsTypeMappingDefinition } from 'kibana/server'; import { estypes } from '@elastic/elasticsearch'; import mappings from './mappings.json'; -import { migrations } from './migrations'; +import { getMigrations } from './migrations'; import { TaskManagerConfig } from '../config.js'; import { getOldestIdleActionTask } from '../queries/oldest_idle_action_task'; @@ -22,7 +22,7 @@ export function setupSavedObjects( hidden: true, convertToAliasScript: `ctx._id = ctx._source.type + ':' + ctx._id; ctx._source.remove("kibana")`, mappings: mappings.task as SavedObjectsTypeMappingDefinition, - migrations, + migrations: getMigrations(), indexPattern: config.index, excludeOnUpgrade: async ({ readonlyEsClient }) => { const oldestNeededActionParams = await getOldestIdleActionTask( diff --git a/x-pack/plugins/task_manager/server/saved_objects/migrations.test.ts b/x-pack/plugins/task_manager/server/saved_objects/migrations.test.ts new file mode 100644 index 0000000000000..73141479d9081 --- /dev/null +++ b/x-pack/plugins/task_manager/server/saved_objects/migrations.test.ts @@ -0,0 +1,169 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import uuid from 'uuid'; +import { getMigrations } from './migrations'; +import { SavedObjectUnsanitizedDoc } from 'kibana/server'; +import { migrationMocks } from 'src/core/server/mocks'; +import { TaskInstanceWithDeprecatedFields } from '../task'; + +const migrationContext = migrationMocks.createContext(); + +describe('successful migrations', () => { + beforeEach(() => { + jest.resetAllMocks(); + }); + describe('7.4.0', () => { + test('extend task instance with updated_at', () => { + const migration740 = getMigrations()['7.4.0']; + const taskInstance = getMockData({}); + expect(migration740(taskInstance, migrationContext).attributes.updated_at).not.toBeNull(); + }); + }); + + describe('7.6.0', () => { + test('rename property Internal to Schedule', () => { + const migration760 = getMigrations()['7.6.0']; + const taskInstance = getMockData({}); + expect(migration760(taskInstance, migrationContext)).toEqual({ + ...taskInstance, + attributes: { + ...taskInstance.attributes, + schedule: taskInstance.attributes.schedule, + }, + }); + }); + }); + + describe('8.0.0', () => { + test('transforms actionsTasksLegacyIdToSavedObjectIds', () => { + const migration800 = getMigrations()['8.0.0']; + const taskInstance = getMockData({ + taskType: 'actions:123456', + params: JSON.stringify({ spaceId: 'user1', actionTaskParamsId: '123456' }), + }); + + expect(migration800(taskInstance, migrationContext)).toEqual({ + ...taskInstance, + attributes: { + ...taskInstance.attributes, + params: '{"spaceId":"user1","actionTaskParamsId":"800f81f8-980e-58ca-b710-d1b0644adea2"}', + }, + }); + }); + + test('it is only applicable for saved objects that live in a custom space', () => { + const migration800 = getMigrations()['8.0.0']; + const taskInstance = getMockData({ + taskType: 'actions:123456', + params: JSON.stringify({ spaceId: 'default', actionTaskParamsId: '123456' }), + }); + + expect(migration800(taskInstance, migrationContext)).toEqual(taskInstance); + }); + + test('it is only applicable for saved objects that live in a custom space even if spaces are disabled', () => { + const migration800 = getMigrations()['8.0.0']; + const taskInstance = getMockData({ + taskType: 'actions:123456', + params: JSON.stringify({ actionTaskParamsId: '123456' }), + }); + + expect(migration800(taskInstance, migrationContext)).toEqual(taskInstance); + }); + + test('transforms alertingTaskLegacyIdToSavedObjectIds', () => { + const migration800 = getMigrations()['8.0.0']; + const taskInstance = getMockData({ + taskType: 'alerting:123456', + params: JSON.stringify({ spaceId: 'user1', alertId: '123456' }), + }); + + expect(migration800(taskInstance, migrationContext)).toEqual({ + ...taskInstance, + attributes: { + ...taskInstance.attributes, + params: '{"spaceId":"user1","alertId":"1a4f9206-e25f-58e6-bad5-3ff21e90648e"}', + }, + }); + }); + + test('skip transformation for defult space scenario', () => { + const migration800 = getMigrations()['8.0.0']; + const taskInstance = getMockData({ + taskType: 'alerting:123456', + params: JSON.stringify({ spaceId: 'default', alertId: '123456' }), + }); + + expect(migration800(taskInstance, migrationContext)).toEqual({ + ...taskInstance, + attributes: { + ...taskInstance.attributes, + params: '{"spaceId":"default","alertId":"123456"}', + }, + }); + }); + }); +}); + +describe('handles errors during migrations', () => { + describe('8.0.0 throws if migration fails', () => { + test('should throw the exception if task instance params format is wrong', () => { + const migration800 = getMigrations()['8.0.0']; + const taskInstance = getMockData({ + taskType: 'alerting:123456', + params: `{ spaceId: 'user1', customId: '123456' }`, + }); + expect(() => { + migration800(taskInstance, migrationContext); + }).toThrowError(); + expect(migrationContext.log.error).toHaveBeenCalledWith( + `savedObject 8.0.0 migration failed for task instance ${taskInstance.id} with error: Unexpected token s in JSON at position 2`, + { + migrations: { + taskInstanceDocument: { + ...taskInstance, + attributes: { + ...taskInstance.attributes, + }, + }, + }, + } + ); + }); + }); +}); + +function getUpdatedAt(): string { + const updatedAt = new Date(); + updatedAt.setHours(updatedAt.getHours() + 2); + return updatedAt.toISOString(); +} + +function getMockData( + overwrites: Record = {} +): SavedObjectUnsanitizedDoc> { + return { + attributes: { + scheduledAt: new Date(), + state: { runs: 0, total_cleaned_up: 0 }, + runAt: new Date(), + startedAt: new Date(), + retryAt: new Date(), + ownerId: '234', + taskType: 'foo', + schedule: { interval: '10s' }, + params: { + bar: true, + }, + ...overwrites, + }, + updated_at: getUpdatedAt(), + id: uuid.v4(), + type: 'task', + }; +} diff --git a/x-pack/plugins/task_manager/server/saved_objects/migrations.ts b/x-pack/plugins/task_manager/server/saved_objects/migrations.ts index 879fca2ae4f6f..a2ed91dba2737 100644 --- a/x-pack/plugins/task_manager/server/saved_objects/migrations.ts +++ b/x-pack/plugins/task_manager/server/saved_objects/migrations.ts @@ -5,16 +5,123 @@ * 2.0. */ -import { SavedObjectMigrationMap, SavedObjectUnsanitizedDoc } from '../../../../../src/core/server'; +import { + LogMeta, + SavedObjectMigrationContext, + SavedObjectMigrationFn, + SavedObjectMigrationMap, + SavedObjectsUtils, + SavedObjectUnsanitizedDoc, +} from '../../../../../src/core/server'; import { TaskInstance, TaskInstanceWithDeprecatedFields } from '../task'; -export const migrations: SavedObjectMigrationMap = { - '7.4.0': (doc) => ({ - ...doc, - updated_at: new Date().toISOString(), - }), - '7.6.0': moveIntervalIntoSchedule, -}; +interface TaskInstanceLogMeta extends LogMeta { + migrations: { taskInstanceDocument: SavedObjectUnsanitizedDoc }; +} + +type TaskInstanceMigration = ( + doc: SavedObjectUnsanitizedDoc +) => SavedObjectUnsanitizedDoc; + +export function getMigrations(): SavedObjectMigrationMap { + return { + '7.4.0': executeMigrationWithErrorHandling( + (doc) => ({ + ...doc, + updated_at: new Date().toISOString(), + }), + '7.4.0' + ), + '7.6.0': executeMigrationWithErrorHandling(moveIntervalIntoSchedule, '7.6.0'), + '8.0.0': executeMigrationWithErrorHandling( + pipeMigrations(alertingTaskLegacyIdToSavedObjectIds, actionsTasksLegacyIdToSavedObjectIds), + '8.0.0' + ), + }; +} + +function executeMigrationWithErrorHandling( + migrationFunc: SavedObjectMigrationFn< + TaskInstanceWithDeprecatedFields, + TaskInstanceWithDeprecatedFields + >, + version: string +) { + return ( + doc: SavedObjectUnsanitizedDoc, + context: SavedObjectMigrationContext + ) => { + try { + return migrationFunc(doc, context); + } catch (ex) { + context.log.error( + `savedObject ${version} migration failed for task instance ${doc.id} with error: ${ex.message}`, + { + migrations: { + taskInstanceDocument: doc, + }, + } + ); + throw ex; + } + }; +} + +function alertingTaskLegacyIdToSavedObjectIds( + doc: SavedObjectUnsanitizedDoc +): SavedObjectUnsanitizedDoc { + if (doc.attributes.taskType.startsWith('alerting:')) { + let params: { spaceId?: string; alertId?: string } = {}; + params = JSON.parse((doc.attributes.params as unknown) as string); + + if (params.alertId && params.spaceId && params.spaceId !== 'default') { + const newId = SavedObjectsUtils.getConvertedObjectId(params.spaceId, 'alert', params.alertId); + return { + ...doc, + attributes: { + ...doc.attributes, + params: JSON.stringify({ + ...params, + alertId: newId, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + }) as any, + }, + }; + } + } + + return doc; +} + +function actionsTasksLegacyIdToSavedObjectIds( + doc: SavedObjectUnsanitizedDoc +): SavedObjectUnsanitizedDoc { + if (doc.attributes.taskType.startsWith('actions:')) { + let params: { spaceId?: string; actionTaskParamsId?: string } = {}; + params = JSON.parse((doc.attributes.params as unknown) as string); + + if (params.actionTaskParamsId && params.spaceId && params.spaceId !== 'default') { + const newId = SavedObjectsUtils.getConvertedObjectId( + params.spaceId, + 'action_task_params', + params.actionTaskParamsId + ); + return { + ...doc, + attributes: { + ...doc.attributes, + params: JSON.stringify({ + ...params, + actionTaskParamsId: newId, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + }) as any, + }, + }; + } + } + + return doc; +} function moveIntervalIntoSchedule({ attributes: { interval, ...attributes }, @@ -34,3 +141,8 @@ function moveIntervalIntoSchedule({ }, }; } + +function pipeMigrations(...migrations: TaskInstanceMigration[]): TaskInstanceMigration { + return (doc: SavedObjectUnsanitizedDoc) => + migrations.reduce((migratedDoc, nextMigration) => nextMigration(migratedDoc), doc); +} diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/rbac_legacy.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/rbac_legacy.ts index 7bc3353898598..e84eaf2cea04d 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/rbac_legacy.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/rbac_legacy.ts @@ -10,6 +10,7 @@ import { UserAtSpaceScenarios, Superuser } from '../../scenarios'; import { FtrProviderContext } from '../../../common/ftr_provider_context'; import { ESTestIndexTool, getUrlPrefix, ObjectRemover, AlertUtils } from '../../../common/lib'; import { setupSpacesAndUsers } from '..'; +import { SavedObjectsUtils } from '../../../../../../src/core/server/saved_objects'; // eslint-disable-next-line import/no-default-export export default function alertTests({ getService }: FtrProviderContext) { @@ -20,13 +21,38 @@ export default function alertTests({ getService }: FtrProviderContext) { const supertestWithoutAuth = getService('supertestWithoutAuth'); const esTestIndexTool = new ESTestIndexTool(es, retry); - const MIGRATED_ACTION_ID = '17f38826-5a8d-4a76-975a-b496e7fffe0b'; + const MIGRATED_ACTION_ID = SavedObjectsUtils.getConvertedObjectId( + 'space1', + 'action', + '17f38826-5a8d-4a76-975a-b496e7fffe0b' + ); + const MIGRATED_ALERT_ID: Record = { - space_1_all_alerts_none_actions: '6ee9630a-a20e-44af-9465-217a3717d2ab', - space_1_all_with_restricted_fixture: '5cc59319-74ee-4edc-8646-a79ea91067cd', - space_1_all: 'd41a6abb-b93b-46df-a80a-926221ea847c', - global_read: '362e362b-a137-4aa2-9434-43e3d0d84a34', - superuser: 'b384be60-ec53-4b26-857e-0253ee55b277', + space_1_all_alerts_none_actions: SavedObjectsUtils.getConvertedObjectId( + 'space1', + 'alert', + '6ee9630a-a20e-44af-9465-217a3717d2ab' + ), + space_1_all_with_restricted_fixture: SavedObjectsUtils.getConvertedObjectId( + 'space1', + 'alert', + '5cc59319-74ee-4edc-8646-a79ea91067cd' + ), + space_1_all: SavedObjectsUtils.getConvertedObjectId( + 'space1', + 'alert', + 'd41a6abb-b93b-46df-a80a-926221ea847c' + ), + global_read: SavedObjectsUtils.getConvertedObjectId( + 'space1', + 'alert', + '362e362b-a137-4aa2-9434-43e3d0d84a34' + ), + superuser: SavedObjectsUtils.getConvertedObjectId( + 'space1', + 'alert', + 'b384be60-ec53-4b26-857e-0253ee55b277' + ), }; describe('alerts', () => { diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/create.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/create.ts index 99a12dc3437de..f45ad28e2cdc5 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/create.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/create.ts @@ -193,7 +193,7 @@ export default function createAlertTests({ getService }: FtrProviderContext) { const esResponse = await es.get>({ index: '.kibana', - id: `${Spaces.space1.id}:alert:${response.body.id}`, + id: `alert:${response.body.id}`, }); expect(esResponse.statusCode).to.eql(200); const rawActions = (esResponse.body._source as any)?.alert.actions ?? []; diff --git a/x-pack/test/functional/es_archives/task_manager_tasks/data.json b/x-pack/test/functional/es_archives/task_manager_tasks/data.json new file mode 100644 index 0000000000000..b59abd341a7af --- /dev/null +++ b/x-pack/test/functional/es_archives/task_manager_tasks/data.json @@ -0,0 +1,61 @@ +{ + "type": "doc", + "value": { + "id": "task:be7e1250-3322-11eb-94c1-db6995e84f6a", + "index": ".kibana_task_manager_1", + "source": { + "migrationVersion": { + "task": "7.16.0" + }, + "references": [ + ], + "task": { + "attempts": 0, + "params": "{\"spaceId\":\"user1\",\"alertId\":\"0359d7fcc04da9878ee9aadbda38ba55\"}", + "retryAt": "2020-11-30T15:43:39.626Z", + "runAt": "2020-11-30T15:43:08.277Z", + "scheduledAt": "2020-11-30T15:43:08.277Z", + "scope": [ + "testing" + ], + "startedAt": null, + "state": "{}", + "status": "idle", + "taskType": "alerting:0359d7fcc04da9878ee9aadbda38ba55" + }, + "type": "task", + "updated_at": "2020-11-30T15:43:08.277Z" + } + } +} + +{ + "type": "doc", + "value": { + "id": "task:be7e1250-3322-11eb-94c1-db6995e8389f", + "index": ".kibana_task_manager_1", + "source": { + "migrationVersion": { + "task": "7.16.0" + }, + "references": [ + ], + "task": { + "attempts": 0, + "params": "{\"spaceId\":\"user1\",\"actionTaskParamsId\":\"6e96ac5e648f57523879661ea72525b7\"}", + "retryAt": "2020-11-30T15:43:39.626Z", + "runAt": "2020-11-30T15:43:08.277Z", + "scheduledAt": "2020-11-30T15:43:08.277Z", + "scope": [ + "testing" + ], + "startedAt": null, + "state": "{}", + "status": "idle", + "taskType": "actions:6e96ac5e648f57523879661ea72525b7" + }, + "type": "task", + "updated_at": "2020-11-30T15:43:08.277Z" + } + } +} \ No newline at end of file diff --git a/x-pack/test/functional/es_archives/task_manager_tasks/mappings.json b/x-pack/test/functional/es_archives/task_manager_tasks/mappings.json new file mode 100644 index 0000000000000..6ec81326d1ca4 --- /dev/null +++ b/x-pack/test/functional/es_archives/task_manager_tasks/mappings.json @@ -0,0 +1,225 @@ +{ + "type": "index", + "value": { + "aliases": { + ".kibana": { + } + }, + "index": ".kibana_1", + "mappings": { + "_meta": { + "migrationMappingPropertyHashes": { + "action": "6e96ac5e648f57523879661ea72525b7", + "action_task_params": "a9d49f184ee89641044be0ca2950fa3a", + "alert": "0359d7fcc04da9878ee9aadbda38ba55", + "api_key_pending_invalidation": "16f515278a295f6245149ad7c5ddedb7", + "apm-indices": "9bb9b2bf1fa636ed8619cbab5ce6a1dd", + "apm-telemetry": "3d1b76c39bfb2cc8296b024d73854724", + "app_search_telemetry": "3d1b76c39bfb2cc8296b024d73854724", + "application_usage_daily": "43b8830d5d0df85a6823d290885fc9fd", + "application_usage_totals": "3d1b76c39bfb2cc8296b024d73854724", + "application_usage_transactional": "3d1b76c39bfb2cc8296b024d73854724", + "search-session": "721df406dbb7e35ac22e4df6c3ad2b2a", + "canvas-element": "7390014e1091044523666d97247392fc", + "canvas-workpad": "b0a1706d356228dbdcb4a17e6b9eb231", + "canvas-workpad-template": "ae2673f678281e2c055d764b153e9715", + "cases": "477f214ff61acc3af26a7b7818e380c1", + "cases-comments": "8a50736330e953bca91747723a319593", + "cases-configure": "387c5f3a3bda7e0ae0dd4e106f914a69", + "cases-user-actions": "32277330ec6b721abe3b846cfd939a71", + "config": "c63748b75f39d0c54de12d12c1ccbc20", + "dashboard": "40554caf09725935e2c02e02563a2d07", + "endpoint:user-artifact": "4a11183eee21e6fbad864f7a30b39ad0", + "endpoint:user-artifact-manifest": "a0d7b04ad405eed54d76e279c3727862", + "enterprise_search_telemetry": "3d1b76c39bfb2cc8296b024d73854724", + "epm-packages": "2b83397e3eaaaa8ef15e38813f3721c3", + "event_log_test": "bef808d4a9c27f204ffbda3359233931", + "exception-list": "67f055ab8c10abd7b2ebfd969b836788", + "exception-list-agnostic": "67f055ab8c10abd7b2ebfd969b836788", + "file-upload-telemetry": "0ed4d3e1983d1217a30982630897092e", + "fleet-agent-actions": "9511b565b1cc6441a42033db3d5de8e9", + "fleet-agent-events": "e20a508b6e805189356be381dbfac8db", + "fleet-agents": "cb661e8ede2b640c42c8e5ef99db0683", + "fleet-enrollment-api-keys": "a69ef7ae661dab31561d6c6f052ef2a7", + "graph-workspace": "cd7ba1330e6682e9cc00b78850874be1", + "index-pattern": "45915a1ad866812242df474eb0479052", + "infrastructure-ui-source": "3d1b76c39bfb2cc8296b024d73854724", + "ingest-agent-policies": "8b0733cce189659593659dad8db426f0", + "ingest-outputs": "8854f34453a47e26f86a29f8f3b80b4e", + "ingest-package-policies": "c91ca97b1ff700f0fc64dc6b13d65a85", + "ingest_manager_settings": "02a03095f0e05b7a538fa801b88a217f", + "inventory-view": "3d1b76c39bfb2cc8296b024d73854724", + "kql-telemetry": "d12a98a6f19a2d273696597547e064ee", + "lens": "52346cfec69ff7b47d5f0c12361a2797", + "lens-ui-telemetry": "509bfa5978586998e05f9e303c07a327", + "map": "4a05b35c3a3a58fbc72dd0202dc3487f", + "maps-telemetry": "5ef305b18111b77789afefbd36b66171", + "metrics-explorer-view": "3d1b76c39bfb2cc8296b024d73854724", + "migrationVersion": "4a1746014a75ade3a714e1db5763276f", + "ml-job": "3bb64c31915acf93fc724af137a0891b", + "ml-telemetry": "257fd1d4b4fdbb9cb4b8a3b27da201e9", + "monitoring-telemetry": "2669d5ec15e82391cf58df4294ee9c68", + "namespace": "2f4316de49999235636386fe51dc06c1", + "namespaces": "2f4316de49999235636386fe51dc06c1", + "originId": "2f4316de49999235636386fe51dc06c1", + "query": "11aaeb7f5f7fa5bb43f25e18ce26e7d9", + "references": "7997cf5a56cc02bdc9c93361bde732b0", + "sample-data-telemetry": "7d3cfeb915303c9641c59681967ffeb4", + "search": "43012c7ebc4cb57054e0a490e4b43023", + "search-telemetry": "3d1b76c39bfb2cc8296b024d73854724", + "siem-detection-engine-rule-actions": "6569b288c169539db10cb262bf79de18", + "siem-detection-engine-rule-status": "ae783f41c6937db6b7a2ef5c93a9e9b0", + "siem-ui-timeline": "d12c5474364d737d17252acf1dc4585c", + "siem-ui-timeline-note": "8874706eedc49059d4cf0f5094559084", + "siem-ui-timeline-pinned-event": "20638091112f0e14f0e443d512301c29", + "space": "c5ca8acafa0beaa4d08d014a97b6bc6b", + "tag": "83d55da58f6530f7055415717ec06474", + "telemetry": "36a616f7026dfa617d6655df850fe16d", + "timelion-sheet": "9a2a2748877c7a7b582fef201ab1d4cf", + "tsvb-validation-telemetry": "3a37ef6c8700ae6fc97d5c7da00e9215", + "type": "2f4316de49999235636386fe51dc06c1", + "ui-metric": "0d409297dc5ebe1e3a1da691c6ee32e3", + "updated_at": "00da57df13e94e9d98437d13ace4bfe0", + "upgrade-assistant-reindex-operation": "215107c281839ea9b3ad5f6419819763", + "upgrade-assistant-telemetry": "56702cec857e0a9dacfb696655b4ff7b", + "uptime-dynamic-settings": "3d1b76c39bfb2cc8296b024d73854724", + "url": "c7f66a0df8b1b52f17c28c4adb111105", + "visualization": "f819cf6636b75c9e76ba733a0c6ef355", + "workplace_search_telemetry": "3d1b76c39bfb2cc8296b024d73854724" + } + }, + "dynamic": "strict", + "properties": { + "type": { + "type": "keyword" + }, + "updated_at": { + "type": "date" + } + } + }, + "settings": { + "index": { + "auto_expand_replicas": "0-1", + "number_of_replicas": "0", + "number_of_shards": "1" + } + } + } +} + +{ + "type": "index", + "value": { + "aliases": { + ".kibana_task_manager": { + } + }, + "index": ".kibana_task_manager_1", + "mappings": { + "_meta": { + "migrationMappingPropertyHashes": { + "migrationVersion": "4a1746014a75ade3a714e1db5763276f", + "namespace": "2f4316de49999235636386fe51dc06c1", + "namespaces": "2f4316de49999235636386fe51dc06c1", + "originId": "2f4316de49999235636386fe51dc06c1", + "references": "7997cf5a56cc02bdc9c93361bde732b0", + "task": "235412e52d09e7165fac8a67a43ad6b4", + "type": "2f4316de49999235636386fe51dc06c1", + "updated_at": "00da57df13e94e9d98437d13ace4bfe0" + } + }, + "dynamic": "strict", + "properties": { + "migrationVersion": { + "dynamic": "true", + "properties": { + "task": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "references": { + "properties": { + "id": { + "type": "keyword" + }, + "name": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + }, + "type": "nested" + }, + "task": { + "properties": { + "attempts": { + "type": "integer" + }, + "ownerId": { + "type": "keyword" + }, + "params": { + "type": "text" + }, + "retryAt": { + "type": "date" + }, + "runAt": { + "type": "date" + }, + "schedule": { + "properties": { + "interval": { + "type": "keyword" + } + } + }, + "scheduledAt": { + "type": "date" + }, + "scope": { + "type": "keyword" + }, + "startedAt": { + "type": "date" + }, + "state": { + "type": "text" + }, + "status": { + "type": "keyword" + }, + "taskType": { + "type": "keyword" + }, + "user": { + "type": "keyword" + } + } + }, + "type": { + "type": "keyword" + }, + "updated_at": { + "type": "date" + } + } + }, + "settings": { + "index": { + "auto_expand_replicas": "0-1", + "number_of_replicas": "0", + "number_of_shards": "1" + } + } + } +} diff --git a/x-pack/test/plugin_api_integration/test_suites/task_manager/index.ts b/x-pack/test/plugin_api_integration/test_suites/task_manager/index.ts index bab34312c363f..b1de32fdcc93c 100644 --- a/x-pack/test/plugin_api_integration/test_suites/task_manager/index.ts +++ b/x-pack/test/plugin_api_integration/test_suites/task_manager/index.ts @@ -13,5 +13,7 @@ export default function ({ loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./health_route')); loadTestFile(require.resolve('./task_management')); loadTestFile(require.resolve('./task_management_removed_types')); + + loadTestFile(require.resolve('./migrations')); }); } diff --git a/x-pack/test/plugin_api_integration/test_suites/task_manager/migrations.ts b/x-pack/test/plugin_api_integration/test_suites/task_manager/migrations.ts new file mode 100644 index 0000000000000..caf62a1d364c0 --- /dev/null +++ b/x-pack/test/plugin_api_integration/test_suites/task_manager/migrations.ts @@ -0,0 +1,70 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import type { ApiResponse, estypes } from '@elastic/elasticsearch'; +import { TaskInstanceWithDeprecatedFields } from '../../../../plugins/task_manager/server/task'; +import { FtrProviderContext } from '../../../common/ftr_provider_context'; +import { SavedObjectsUtils } from '../../../../../src/core/server/saved_objects'; + +export default function createGetTests({ getService }: FtrProviderContext) { + const es = getService('es'); + const esArchiver = getService('esArchiver'); + const ALERT_ID = '0359d7fcc04da9878ee9aadbda38ba55'; + const ACTION_TASK_PARAMS_ID = '6e96ac5e648f57523879661ea72525b7'; + + describe('migrations', () => { + before(async () => { + await esArchiver.load('x-pack/test/functional/es_archives/task_manager_tasks'); + }); + + after(async () => { + await esArchiver.unload('x-pack/test/functional/es_archives/task_manager_tasks'); + }); + + it('8.0.0 migrates actions tasks with legacy id to saved object ids', async () => { + // NOTE: We hae to use elastic search directly against the ".kibana" index because alerts do not expose the references which we want to test exists + const response = await es.get<{ task: TaskInstanceWithDeprecatedFields }>({ + index: '.kibana_task_manager', + id: 'task:be7e1250-3322-11eb-94c1-db6995e84f6a', + }); + expect(response.statusCode).to.eql(200); + expect(response.body._source?.task.params).to.eql( + `{"spaceId":"user1","alertId":"${SavedObjectsUtils.getConvertedObjectId( + 'user1', + 'alert', + ALERT_ID + )}"}` + ); + }); + + it('8.0.0 migrates actions tasks from legacy id to saved object ids', async () => { + const searchResult: ApiResponse< + estypes.SearchResponse<{ task: TaskInstanceWithDeprecatedFields }> + > = await es.search({ + index: '.kibana_task_manager', + body: { + query: { + term: { + _id: 'task:be7e1250-3322-11eb-94c1-db6995e8389f', + }, + }, + }, + }); + expect(searchResult.statusCode).to.equal(200); + expect((searchResult.body.hits.total as estypes.SearchTotalHits).value).to.equal(1); + const hit = searchResult.body.hits.hits[0]; + expect(hit!._source!.task.params!).to.equal( + `{"spaceId":"user1","actionTaskParamsId":"${SavedObjectsUtils.getConvertedObjectId( + 'user1', + 'action_task_params', + ACTION_TASK_PARAMS_ID + )}"}` + ); + }); + }); +} From 53c011830dfbe9da04021d3529e5ce94e863236e Mon Sep 17 00:00:00 2001 From: Kevin Logan <56395104+kevinlog@users.noreply.github.com> Date: Tue, 31 Aug 2021 22:07:00 -0400 Subject: [PATCH 20/81] [Security Solution] Correct memory exception field names (#110705) --- .../common/ecs/event/index.ts | 2 +- .../common/endpoint/generate_data.ts | 2 +- .../event_details/alert_summary_view.test.tsx | 4 +- .../event_details/alert_summary_view.tsx | 2 +- .../exceptionable_endpoint_fields.json | 12 +-- .../components/exceptions/helpers.test.tsx | 85 +------------------ .../common/components/exceptions/helpers.tsx | 42 +-------- 7 files changed, 18 insertions(+), 131 deletions(-) diff --git a/x-pack/plugins/security_solution/common/ecs/event/index.ts b/x-pack/plugins/security_solution/common/ecs/event/index.ts index 14f38480f90c8..9e2ebb059b3b3 100644 --- a/x-pack/plugins/security_solution/common/ecs/event/index.ts +++ b/x-pack/plugins/security_solution/common/ecs/event/index.ts @@ -53,7 +53,7 @@ export enum EventCode { // Memory Protection alert MEMORY_SIGNATURE = 'memory_signature', // Memory Protection alert - MALICIOUS_THREAD = 'malicious_thread', + SHELLCODE_THREAD = 'shellcode_thread', // behavior BEHAVIOR = 'behavior', } diff --git a/x-pack/plugins/security_solution/common/endpoint/generate_data.ts b/x-pack/plugins/security_solution/common/endpoint/generate_data.ts index afe85e1abaa53..8f985db732b61 100644 --- a/x-pack/plugins/security_solution/common/endpoint/generate_data.ts +++ b/x-pack/plugins/security_solution/common/endpoint/generate_data.ts @@ -678,7 +678,7 @@ export class EndpointDocGenerator extends BaseDataGenerator { action: 'start', kind: 'alert', category: 'malware', - code: isShellcode ? 'malicious_thread' : 'memory_signature', + code: isShellcode ? 'shellcode_thread' : 'memory_signature', id: this.seededUUIDv4(), dataset: 'endpoint', module: 'endpoint', diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/alert_summary_view.test.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/alert_summary_view.test.tsx index db5eb2d882c6f..2b399a0571178 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/alert_summary_view.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/alert_summary_view.test.tsx @@ -86,8 +86,8 @@ describe('AlertSummaryView', () => { return { category: 'event', field: 'event.code', - values: ['malicious_thread'], - originalValue: ['malicious_thread'], + values: ['shellcode_thread'], + originalValue: ['shellcode_thread'], }; } return item; diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/alert_summary_view.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/alert_summary_view.tsx index d8c1cc7fbfa60..da6c091ab069a 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/alert_summary_view.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/alert_summary_view.tsx @@ -157,7 +157,7 @@ function getEventFieldsToDisplay({ }): EventSummaryField[] { switch (eventCode) { // memory protection fields - case EventCode.MALICIOUS_THREAD: + case EventCode.SHELLCODE_THREAD: return memoryShellCodeAlertFields; case EventCode.MEMORY_SIGNATURE: return memorySignatureAlertFields; diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/exceptionable_endpoint_fields.json b/x-pack/plugins/security_solution/public/common/components/exceptions/exceptionable_endpoint_fields.json index d46b39b90fe5a..043ea11a51fd1 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/exceptionable_endpoint_fields.json +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/exceptionable_endpoint_fields.json @@ -19,13 +19,13 @@ "Target.process.pe.original_file_name", "Target.process.pe.product", "Target.process.pgid", - "Target.process.thread.Ext.start_address_details.allocation_type", + "Target.process.Ext.memory_region.allocation_type", "Target.process.thread.Ext.start_address_bytes_disasm_hash", "Target.process.thread.Ext.start_address_allocation_offset", - "Target.process.thread.Ext.start_address_details.allocation_size", - "Target.process.thread.Ext.start_address_details.region_size", - "Target.process.thread.Ext.start_address_details.region_protection", - "Target.process.thread.Ext.start_address_details.memory_pe.imphash", + "Target.process.Ext.memory_region.allocation_size", + "Target.process.Ext.memory_region.region_size", + "Target.process.Ext.memory_region.region_protection", + "Target.process.Ext.memory_region.memory_pe.imphash", "Target.process.thread.Ext.start_address_bytes", "agent.id", "agent.type", @@ -82,6 +82,8 @@ "process.Ext.services", "process.Ext.user", "process.Ext.code_signature", + "process.Ext.token.integrity_level_name", + "process.Ext.memory_region.malware_signature.all_names", "process.executable", "process.hash.md5", "process.hash.sha1", diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.test.tsx index 9696604ddf222..209d7d8fa273b 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.test.tsx @@ -1031,7 +1031,7 @@ describe('Exception helpers', () => { ]); }); - test('it should return pre-populated memory shellcode items for event code `malicious_thread`', () => { + test('it should return pre-populated memory shellcode items for event code `shellcode_thread`', () => { const defaultItems = defaultEndpointExceptionItems('list_id', 'my_rule', { _id: '123', process: { @@ -1049,7 +1049,7 @@ describe('Exception helpers', () => { self_injection: true, }, event: { - code: 'malicious_thread', + code: 'shellcode_thread', }, Target: { process: { @@ -1108,52 +1108,10 @@ describe('Exception helpers', () => { value: 'high', id: '123', }, - { - field: 'Target.process.thread.Ext.start_address_details', - type: 'nested', - entries: [ - { - field: 'allocation_type', - operator: 'included', - type: 'match', - value: 'PRIVATE', - id: '123', - }, - { - field: 'allocation_size', - operator: 'included', - type: 'match', - value: '4000', - id: '123', - }, - { - field: 'region_size', - operator: 'included', - type: 'match', - value: '4000', - id: '123', - }, - { - field: 'region_protection', - operator: 'included', - type: 'match', - value: 'RWX', - id: '123', - }, - { - field: 'memory_pe.imphash', - operator: 'included', - type: 'match', - value: 'a hash', - id: '123', - }, - ], - id: '123', - }, ]); }); - test('it should return pre-populated memory shellcode items for event code `malicious_thread` and skip empty', () => { + test('it should return pre-populated memory shellcode items for event code `shellcode_thread` and skip empty', () => { const defaultItems = defaultEndpointExceptionItems('list_id', 'my_rule', { _id: '123', process: { @@ -1171,7 +1129,7 @@ describe('Exception helpers', () => { self_injection: true, }, event: { - code: 'malicious_thread', + code: 'shellcode_thread', }, Target: { process: { @@ -1217,41 +1175,6 @@ describe('Exception helpers', () => { value: 'high', id: '123', }, - { - field: 'Target.process.thread.Ext.start_address_details', - type: 'nested', - entries: [ - { - field: 'allocation_size', - operator: 'included', - type: 'match', - value: '4000', - id: '123', - }, - { - field: 'region_size', - operator: 'included', - type: 'match', - value: '4000', - id: '123', - }, - { - field: 'region_protection', - operator: 'included', - type: 'match', - value: 'RWX', - id: '123', - }, - { - field: 'memory_pe.imphash', - operator: 'included', - type: 'match', - value: 'a hash', - id: '123', - }, - ], - id: '123', - }, ]); }); }); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.tsx index 3d219b90a2fc8..58da977fcb8f0 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.tsx @@ -577,7 +577,7 @@ export const getPrepopulatedMemoryShellcodeException = ({ eventCode: string; alertEcsData: Flattened; }): ExceptionsBuilderExceptionItem => { - const { process, Target } = alertEcsData; + const { process } = alertEcsData; const entries = filterEmptyExceptionEntries([ { field: 'Memory_protection.feature', @@ -609,44 +609,6 @@ export const getPrepopulatedMemoryShellcodeException = ({ type: 'match' as const, value: process?.Ext?.token?.integrity_level_name ?? '', }, - { - field: 'Target.process.thread.Ext.start_address_details', - type: 'nested' as const, - entries: [ - { - field: 'allocation_type', - operator: 'included' as const, - type: 'match' as const, - value: Target?.process?.thread?.Ext?.start_address_details?.allocation_type ?? '', - }, - { - field: 'allocation_size', - operator: 'included' as const, - type: 'match' as const, - value: String(Target?.process?.thread?.Ext?.start_address_details?.allocation_size) ?? '', - }, - { - field: 'region_size', - operator: 'included' as const, - type: 'match' as const, - value: String(Target?.process?.thread?.Ext?.start_address_details?.region_size) ?? '', - }, - { - field: 'region_protection', - operator: 'included' as const, - type: 'match' as const, - value: - String(Target?.process?.thread?.Ext?.start_address_details?.region_protection) ?? '', - }, - { - field: 'memory_pe.imphash', - operator: 'included' as const, - type: 'match' as const, - value: - String(Target?.process?.thread?.Ext?.start_address_details?.memory_pe?.imphash) ?? '', - }, - ], - }, ]); return { @@ -845,7 +807,7 @@ export const defaultEndpointExceptionItems = ( alertEcsData, }), ]; - case 'malicious_thread': + case 'shellcode_thread': return [ getPrepopulatedMemoryShellcodeException({ listId, From ff73025533e62a3b7d66648cbc0fad394ade9c4f Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Wed, 1 Sep 2021 03:34:54 +0100 Subject: [PATCH 21/81] chore(NA): check for used dependencies on multiple level plugins (#110626) --- src/dev/build/tasks/package_json/find_used_dependencies.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/dev/build/tasks/package_json/find_used_dependencies.ts b/src/dev/build/tasks/package_json/find_used_dependencies.ts index 004e17b87ac8b..8cb8b3c986de7 100644 --- a/src/dev/build/tasks/package_json/find_used_dependencies.ts +++ b/src/dev/build/tasks/package_json/find_used_dependencies.ts @@ -29,9 +29,9 @@ export async function findUsedDependencies(listedPkgDependencies: any, baseDir: ]; const discoveredPluginEntries = await globby([ - normalize(Path.resolve(baseDir, `src/plugins/*/server/index.js`)), + normalize(Path.resolve(baseDir, `src/plugins/**/server/index.js`)), `!${normalize(Path.resolve(baseDir, `/src/plugins/**/public`))}`, - normalize(Path.resolve(baseDir, `x-pack/plugins/*/server/index.js`)), + normalize(Path.resolve(baseDir, `x-pack/plugins/**/server/index.js`)), `!${normalize(Path.resolve(baseDir, `/x-pack/plugins/**/public`))}`, ]); From 5b6588d8cbf10ad6d424723039a57f537b4fb46a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cau=C3=AA=20Marcondes?= <55978943+cauemarcondes@users.noreply.github.com> Date: Tue, 31 Aug 2021 23:56:47 -0400 Subject: [PATCH 22/81] [APM] Custom links creation don't work (#110676) * fixing custom links issue * Removing unused imports --- x-pack/plugins/apm/ftr_e2e/config.ts | 1 + .../power_user/settings/custom_links.spec.ts | 37 ++++++ .../apm/ftr_e2e/cypress/support/commands.ts | 4 +- .../apm/ftr_e2e/cypress/support/types.d.ts | 2 +- .../customize_ui/custom_link/EmptyPrompt.tsx | 1 + .../FlyoutFooter.tsx | 1 + .../create_edit_custom_link_flyout/index.tsx | 107 +++++++++--------- .../custom_link/custom_link_table.tsx | 1 + 8 files changed, 96 insertions(+), 58 deletions(-) create mode 100644 x-pack/plugins/apm/ftr_e2e/cypress/integration/power_user/settings/custom_links.spec.ts diff --git a/x-pack/plugins/apm/ftr_e2e/config.ts b/x-pack/plugins/apm/ftr_e2e/config.ts index 5f919fb7f075d..12cc8845264c2 100644 --- a/x-pack/plugins/apm/ftr_e2e/config.ts +++ b/x-pack/plugins/apm/ftr_e2e/config.ts @@ -35,6 +35,7 @@ async function config({ readConfigFile }: FtrConfigProviderContext) { ...xpackFunctionalTestsConfig.get('kbnTestServer.serverArgs'), '--home.disableWelcomeScreen=true', '--csp.strict=false', + '--csp.warnLegacyBrowsers=false', // define custom kibana server args here `--elasticsearch.ssl.certificateAuthorities=${CA_CERT_PATH}`, ], diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/integration/power_user/settings/custom_links.spec.ts b/x-pack/plugins/apm/ftr_e2e/cypress/integration/power_user/settings/custom_links.spec.ts new file mode 100644 index 0000000000000..eeb46db04b9d4 --- /dev/null +++ b/x-pack/plugins/apm/ftr_e2e/cypress/integration/power_user/settings/custom_links.spec.ts @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +const basePath = '/app/apm/settings/customize-ui'; + +describe('Custom links', () => { + beforeEach(() => { + cy.loginAsPowerUser(); + }); + + it('shows empty message and create button', () => { + cy.visit(basePath); + cy.contains('No links found'); + cy.contains('Create custom link'); + }); + + it('creates custom link', () => { + cy.visit(basePath); + const emptyPrompt = cy.get('[data-test-subj="customLinksEmptyPrompt"]'); + cy.contains('Create custom link').click(); + cy.contains('Create link'); + cy.contains('Save').should('be.disabled'); + cy.get('input[name="label"]').type('foo'); + cy.get('input[name="url"]').type('https://foo.com'); + cy.contains('Save').should('not.be.disabled'); + cy.contains('Save').click(); + emptyPrompt.should('not.exist'); + cy.contains('foo'); + cy.contains('https://foo.com'); + cy.get('[data-test-subj="editCustomLink"]').click(); + cy.contains('Delete').click(); + }); +}); diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/support/commands.ts b/x-pack/plugins/apm/ftr_e2e/cypress/support/commands.ts index 31eab9507ef5e..93dbe4ba51226 100644 --- a/x-pack/plugins/apm/ftr_e2e/cypress/support/commands.ts +++ b/x-pack/plugins/apm/ftr_e2e/cypress/support/commands.ts @@ -11,8 +11,8 @@ Cypress.Commands.add('loginAsReadOnlyUser', () => { cy.loginAs({ username: 'apm_read_user', password: 'changeme' }); }); -Cypress.Commands.add('loginAsSuperUser', () => { - cy.loginAs({ username: 'elastic', password: 'changeme' }); +Cypress.Commands.add('loginAsPowerUser', () => { + cy.loginAs({ username: 'apm_power_user', password: 'changeme' }); }); Cypress.Commands.add( diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/support/types.d.ts b/x-pack/plugins/apm/ftr_e2e/cypress/support/types.d.ts index b47e664e0a0f8..2d9ef090eef65 100644 --- a/x-pack/plugins/apm/ftr_e2e/cypress/support/types.d.ts +++ b/x-pack/plugins/apm/ftr_e2e/cypress/support/types.d.ts @@ -8,7 +8,7 @@ declare namespace Cypress { interface Chainable { loginAsReadOnlyUser(): void; - loginAsSuperUser(): void; + loginAsPowerUser(): void; loginAs(params: { username: string; password: string }): void; changeTimeRange(value: string): void; expectAPIsToHaveBeenCalledWith(params: { diff --git a/x-pack/plugins/apm/public/components/app/Settings/customize_ui/custom_link/EmptyPrompt.tsx b/x-pack/plugins/apm/public/components/app/Settings/customize_ui/custom_link/EmptyPrompt.tsx index 9d6a3eef3f7eb..498e17b9c359d 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/customize_ui/custom_link/EmptyPrompt.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/customize_ui/custom_link/EmptyPrompt.tsx @@ -17,6 +17,7 @@ export function EmptyPrompt({ }) { return ( )} -
- - - -

- {i18n.translate( - 'xpack.apm.settings.customizeUI.customLink.flyout.title', + + + + +

+ {i18n.translate( + 'xpack.apm.settings.customizeUI.customLink.flyout.title', + { + defaultMessage: 'Create link', + } + )} +

+
+
+ + +

+ {i18n.translate( + 'xpack.apm.settings.customizeUI.customLink.flyout.label', + { + defaultMessage: + 'Links will be available in the context of transaction details throughout the APM app. You can create an unlimited number of links. You can refer to dynamic variables by using any of the transaction metadata to fill in your URLs. More information, including examples, are available in the', + } + )}{' '} + - - - - -

- {i18n.translate( - 'xpack.apm.settings.customizeUI.customLink.flyout.label', - { - defaultMessage: - 'Links will be available in the context of transaction details throughout the APM app. You can create an unlimited number of links. You can refer to dynamic variables by using any of the transaction metadata to fill in your URLs. More information, including examples, are available in the', - } - )}{' '} - -

-
+ /> +

+ - + - + - + - + - + - -
+ + - -
- - + + + ); } diff --git a/x-pack/plugins/apm/public/components/app/Settings/customize_ui/custom_link/custom_link_table.tsx b/x-pack/plugins/apm/public/components/app/Settings/customize_ui/custom_link/custom_link_table.tsx index 4a242bb661e3a..86a7a8742eaea 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/customize_ui/custom_link/custom_link_table.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/customize_ui/custom_link/custom_link_table.tsx @@ -79,6 +79,7 @@ export function CustomLinkTable({ items = [], onCustomLinkSelected }: Props) { icon: 'pencil', color: 'primary', type: 'icon', + 'data-test-subj': 'editCustomLink', onClick: (customLink: CustomLink) => { onCustomLinkSelected(customLink); }, From 31057f72154b42fce9adaf9895a319e7002ffed5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cau=C3=AA=20Marcondes?= <55978943+cauemarcondes@users.noreply.github.com> Date: Wed, 1 Sep 2021 00:01:10 -0400 Subject: [PATCH 23/81] [APM] "Backends" naming (#110523) * renaming backends to dependencies * changing name on service maps --- x-pack/plugins/apm/common/backends.ts | 4 ++-- .../service_overview/service_overview.spec.ts | 6 +++++- .../app/backend_detail_overview/index.tsx | 4 ++-- .../backend_inventory_dependencies_table/index.tsx | 4 ++-- .../app/service_map/Popover/backend_contents.tsx | 4 ++-- .../app/service_map/Popover/popover.test.tsx | 4 ++-- .../service_overview_dependencies_table/index.tsx | 6 +++--- .../apm/public/components/routing/home/index.tsx | 8 ++++---- x-pack/plugins/apm/public/plugin.ts | 13 ++++++++----- x-pack/plugins/translations/translations/ja-JP.json | 1 - x-pack/plugins/translations/translations/zh-CN.json | 1 - 11 files changed, 30 insertions(+), 25 deletions(-) diff --git a/x-pack/plugins/apm/common/backends.ts b/x-pack/plugins/apm/common/backends.ts index 35a52cf3778f1..53418fe7f9b62 100644 --- a/x-pack/plugins/apm/common/backends.ts +++ b/x-pack/plugins/apm/common/backends.ts @@ -14,9 +14,9 @@ import { import { environmentQuery } from './utils/environment_query'; export const kueryBarPlaceholder = i18n.translate( - 'xpack.apm.backends.kueryBarPlaceholder', + 'xpack.apm.dependencies.kueryBarPlaceholder', { - defaultMessage: `Search backend metrics (e.g. span.destination.service.resource:elasticsearch)`, + defaultMessage: `Search dependency metrics (e.g. span.destination.service.resource:elasticsearch)`, } ); diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/service_overview/service_overview.spec.ts b/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/service_overview/service_overview.spec.ts index de8969cbecdeb..b90ad12b46025 100644 --- a/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/service_overview/service_overview.spec.ts +++ b/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/service_overview/service_overview.spec.ts @@ -74,6 +74,10 @@ describe('Service Overview', () => { cy.contains('Service Map'); // Waits until the agent request is finished to check the tab. cy.wait('@agentRequest'); - cy.contains('Dependencies').should('not.exist'); + cy.get('.euiTabs .euiTab__content').then((elements) => { + elements.map((index, element) => { + expect(element.innerText).to.not.equal('Dependencies'); + }); + }); }); }); diff --git a/x-pack/plugins/apm/public/components/app/backend_detail_overview/index.tsx b/x-pack/plugins/apm/public/components/app/backend_detail_overview/index.tsx index 1adb41acab70a..2c9ec0a232974 100644 --- a/x-pack/plugins/apm/public/components/app/backend_detail_overview/index.tsx +++ b/x-pack/plugins/apm/public/components/app/backend_detail_overview/index.tsx @@ -18,7 +18,7 @@ import { useApmParams } from '../../../hooks/use_apm_params'; import { useApmRouter } from '../../../hooks/use_apm_router'; import { SearchBar } from '../../shared/search_bar'; import { BackendLatencyChart } from './backend_latency_chart'; -import { BackendInventoryTitle } from '../../routing/home'; +import { DependenciesInventoryTitle } from '../../routing/home'; import { BackendDetailDependenciesTable } from './backend_detail_dependencies_table'; import { BackendThroughputChart } from './backend_throughput_chart'; import { BackendFailedTransactionRateChart } from './backend_error_rate_chart'; @@ -39,7 +39,7 @@ export function BackendDetailOverview() { useBreadcrumb([ { - title: BackendInventoryTitle, + title: DependenciesInventoryTitle, href: apmRouter.link('/backends', { query: { rangeFrom, rangeTo, environment, kuery }, }), diff --git a/x-pack/plugins/apm/public/components/app/backend_inventory/backend_inventory_dependencies_table/index.tsx b/x-pack/plugins/apm/public/components/app/backend_inventory/backend_inventory_dependencies_table/index.tsx index 7ccf3f166fc65..ea135104982e5 100644 --- a/x-pack/plugins/apm/public/components/app/backend_inventory/backend_inventory_dependencies_table/index.tsx +++ b/x-pack/plugins/apm/public/components/app/backend_inventory/backend_inventory_dependencies_table/index.tsx @@ -98,9 +98,9 @@ export function BackendInventoryDependenciesTable() { dependencies={dependencies} title={null} nameColumnTitle={i18n.translate( - 'xpack.apm.backendInventory.dependenciesTableColumnBackend', + 'xpack.apm.backendInventory.dependencyTableColumn', { - defaultMessage: 'Backend', + defaultMessage: 'Dependency', } )} status={status} diff --git a/x-pack/plugins/apm/public/components/app/service_map/Popover/backend_contents.tsx b/x-pack/plugins/apm/public/components/app/service_map/Popover/backend_contents.tsx index 0a42dbab9a452..9bc30ee67d2c7 100644 --- a/x-pack/plugins/apm/public/components/app/service_map/Popover/backend_contents.tsx +++ b/x-pack/plugins/apm/public/components/app/service_map/Popover/backend_contents.tsx @@ -85,8 +85,8 @@ export function BackendContents({ }); }} > - {i18n.translate('xpack.apm.serviceMap.backendDetailsButtonText', { - defaultMessage: 'Backend Details', + {i18n.translate('xpack.apm.serviceMap.dependencyDetailsButtonText', { + defaultMessage: 'Dependency Details', })} diff --git a/x-pack/plugins/apm/public/components/app/service_map/Popover/popover.test.tsx b/x-pack/plugins/apm/public/components/app/service_map/Popover/popover.test.tsx index 9678258c4740c..5bec70b9eb841 100644 --- a/x-pack/plugins/apm/public/components/app/service_map/Popover/popover.test.tsx +++ b/x-pack/plugins/apm/public/components/app/service_map/Popover/popover.test.tsx @@ -14,12 +14,12 @@ const { Backend, ExternalsList, Resource, Service } = composeStories(stories); describe('Popover', () => { describe('with backend data', () => { - it('renders a backend link', async () => { + it('renders a dependency link', async () => { render(); await waitFor(() => { expect( - screen.getByRole('link', { name: /backend details/i }) + screen.getByRole('link', { name: /Dependency Details/i }) ).toBeInTheDocument(); }); }); diff --git a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_dependencies_table/index.tsx b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_dependencies_table/index.tsx index 0d0842582335b..08f29d7727cda 100644 --- a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_dependencies_table/index.tsx +++ b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_dependencies_table/index.tsx @@ -125,13 +125,13 @@ export function ServiceOverviewDependenciesTable({ title={i18n.translate( 'xpack.apm.serviceOverview.dependenciesTableTitle', { - defaultMessage: 'Downstream services and backends', + defaultMessage: 'Dependencies', } )} nameColumnTitle={i18n.translate( - 'xpack.apm.serviceOverview.dependenciesTableColumnBackend', + 'xpack.apm.serviceOverview.dependenciesTableColumn', { - defaultMessage: 'Backend', + defaultMessage: 'Dependency', } )} status={status} diff --git a/x-pack/plugins/apm/public/components/routing/home/index.tsx b/x-pack/plugins/apm/public/components/routing/home/index.tsx index d1304e192ddce..1430f5d8e4756 100644 --- a/x-pack/plugins/apm/public/components/routing/home/index.tsx +++ b/x-pack/plugins/apm/public/components/routing/home/index.tsx @@ -45,10 +45,10 @@ export const ServiceInventoryTitle = i18n.translate( } ); -export const BackendInventoryTitle = i18n.translate( - 'xpack.apm.views.backendInventory.title', +export const DependenciesInventoryTitle = i18n.translate( + 'xpack.apm.views.dependenciesInventory.title', { - defaultMessage: 'Backends', + defaultMessage: 'Dependencies', } ); @@ -114,7 +114,7 @@ export const home = { }, page({ path: '/', - title: BackendInventoryTitle, + title: DependenciesInventoryTitle, element: , }), ], diff --git a/x-pack/plugins/apm/public/plugin.ts b/x-pack/plugins/apm/public/plugin.ts index a329ad57e2b33..da2ea0ba8ae5c 100644 --- a/x-pack/plugins/apm/public/plugin.ts +++ b/x-pack/plugins/apm/public/plugin.ts @@ -93,9 +93,12 @@ const serviceMapTitle = i18n.translate('xpack.apm.navigation.serviceMapTitle', { defaultMessage: 'Service Map', }); -const backendsTitle = i18n.translate('xpack.apm.navigation.backendsTitle', { - defaultMessage: 'Backends', -}); +const dependenciesTitle = i18n.translate( + 'xpack.apm.navigation.dependenciesTitle', + { + defaultMessage: 'Dependencies', + } +); export class ApmPlugin implements Plugin { constructor( @@ -126,7 +129,7 @@ export class ApmPlugin implements Plugin { { label: servicesTitle, app: 'apm', path: '/services' }, { label: tracesTitle, app: 'apm', path: '/traces' }, { - label: backendsTitle, + label: dependenciesTitle, app: 'apm', path: '/backends', isNewFeature: true, @@ -270,7 +273,7 @@ export class ApmPlugin implements Plugin { { id: 'services', title: servicesTitle, path: '/services' }, { id: 'traces', title: tracesTitle, path: '/traces' }, { id: 'service-map', title: serviceMapTitle, path: '/service-map' }, - { id: 'backends', title: backendsTitle, path: '/backends' }, + { id: 'backends', title: dependenciesTitle, path: '/backends' }, ], async mount(appMountParameters: AppMountParameters) { diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 770f24114d8ef..6f9badea99a0f 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -5657,7 +5657,6 @@ "xpack.apm.serviceNodeMetrics.unidentifiedServiceNodesWarningTitle": "JVM を特定できませんでした", "xpack.apm.serviceNodeNameMissing": " (空) ", "xpack.apm.serviceOveriew.errorsTableOccurrences": "{occurrencesCount} occ.", - "xpack.apm.serviceOverview.dependenciesTableColumnBackend": "バックエンド", "xpack.apm.serviceOverview.dependenciesTableTitle": "依存関係", "xpack.apm.serviceOverview.errorsTableColumnLastSeen": "前回の認識", "xpack.apm.serviceOverview.errorsTableColumnName": "名前", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index c28fdbed5f31c..572e3699897a0 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -5685,7 +5685,6 @@ "xpack.apm.serviceNodeMetrics.unidentifiedServiceNodesWarningTitle": "找不到 JVM", "xpack.apm.serviceNodeNameMissing": "(空)", "xpack.apm.serviceOveriew.errorsTableOccurrences": "{occurrencesCount} 次", - "xpack.apm.serviceOverview.dependenciesTableColumnBackend": "后端", "xpack.apm.serviceOverview.dependenciesTableTitle": "依赖项", "xpack.apm.serviceOverview.errorsTableColumnLastSeen": "最后看到时间", "xpack.apm.serviceOverview.errorsTableColumnName": "名称", From 0d4fb4f338f5f59eec9a4847a125ce6cfc615ae0 Mon Sep 17 00:00:00 2001 From: Dario Gieselaar Date: Wed, 1 Sep 2021 08:41:37 +0200 Subject: [PATCH 24/81] [APM] Filter throughput on transaction name (#110645) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../service_overview_throughput_chart.tsx | 4 ++++ .../components/app/transaction_details/index.tsx | 1 + .../shared/charts/transaction_charts/index.tsx | 3 +++ .../plugins/apm/server/lib/services/get_throughput.ts | 11 +++++++++++ x-pack/plugins/apm/server/routes/services.ts | 8 ++++---- 5 files changed, 23 insertions(+), 4 deletions(-) diff --git a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_throughput_chart.tsx b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_throughput_chart.tsx index 9b8706fe11035..6751e76cfa335 100644 --- a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_throughput_chart.tsx +++ b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_throughput_chart.tsx @@ -37,10 +37,12 @@ export function ServiceOverviewThroughputChart({ height, environment, kuery, + transactionName, }: { height?: number; environment: string; kuery: string; + transactionName?: string; }) { const theme = useTheme(); @@ -80,6 +82,7 @@ export function ServiceOverviewThroughputChart({ transactionType, comparisonStart, comparisonEnd, + transactionName, }, }, }); @@ -94,6 +97,7 @@ export function ServiceOverviewThroughputChart({ transactionType, comparisonStart, comparisonEnd, + transactionName, ] ); diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/index.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/index.tsx index c4ecc71941b8c..9da1ee25246dd 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_details/index.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_details/index.tsx @@ -50,6 +50,7 @@ export function TransactionDetails() { environment={query.environment} start={start} end={end} + transactionName={transactionName} /> diff --git a/x-pack/plugins/apm/public/components/shared/charts/transaction_charts/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/transaction_charts/index.tsx index 6e2ed04776e1c..4fdce0dfa705e 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/transaction_charts/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/transaction_charts/index.tsx @@ -19,11 +19,13 @@ export function TransactionCharts({ environment, start, end, + transactionName, }: { kuery: string; environment: string; start: string; end: string; + transactionName?: string; }) { return ( <> @@ -44,6 +46,7 @@ export function TransactionCharts({ diff --git a/x-pack/plugins/apm/server/lib/services/get_throughput.ts b/x-pack/plugins/apm/server/lib/services/get_throughput.ts index e866918fc29bb..76d6000a161e6 100644 --- a/x-pack/plugins/apm/server/lib/services/get_throughput.ts +++ b/x-pack/plugins/apm/server/lib/services/get_throughput.ts @@ -8,6 +8,7 @@ import { ESFilter } from '../../../../../../src/core/types/elasticsearch'; import { SERVICE_NAME, + TRANSACTION_NAME, TRANSACTION_TYPE, } from '../../../common/elasticsearch_fieldnames'; import { kqlQuery, rangeQuery } from '../../../../observability/server'; @@ -25,6 +26,7 @@ interface Options { serviceName: string; setup: Setup; transactionType: string; + transactionName?: string; start: number; end: number; intervalString: string; @@ -38,6 +40,7 @@ export async function getThroughput({ serviceName, setup, transactionType, + transactionName, start, end, intervalString, @@ -56,6 +59,14 @@ export async function getThroughput({ ...kqlQuery(kuery), ]; + if (transactionName) { + filter.push({ + term: { + [TRANSACTION_NAME]: transactionName, + }, + }); + } + const params = { apm: { events: [ diff --git a/x-pack/plugins/apm/server/routes/services.ts b/x-pack/plugins/apm/server/routes/services.ts index 32a7dcefb5cc8..550781cc1a020 100644 --- a/x-pack/plugins/apm/server/routes/services.ts +++ b/x-pack/plugins/apm/server/routes/services.ts @@ -451,10 +451,8 @@ const serviceThroughputRoute = createApmServerRoute({ }), query: t.intersection([ t.type({ transactionType: t.string }), - environmentRt, - kueryRt, - rangeRt, - comparisonRangeRt, + t.partial({ transactionName: t.string }), + t.intersection([environmentRt, kueryRt, rangeRt, comparisonRangeRt]), ]), }), options: { tags: ['access:apm'] }, @@ -466,6 +464,7 @@ const serviceThroughputRoute = createApmServerRoute({ environment, kuery, transactionType, + transactionName, comparisonStart, comparisonEnd, } = params.query; @@ -493,6 +492,7 @@ const serviceThroughputRoute = createApmServerRoute({ serviceName, setup, transactionType, + transactionName, throughputUnit, intervalString, }; From 29b45883be3fc8a4865402bc6c0f1e758055926e Mon Sep 17 00:00:00 2001 From: Thomas Watson Date: Wed, 1 Sep 2021 08:50:50 +0200 Subject: [PATCH 25/81] Bump Node.js from version 14.17.5 to 14.17.6 (#110654) --- .node-version | 2 +- .nvmrc | 2 +- WORKSPACE.bazel | 12 ++++++------ package.json | 2 +- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.node-version b/.node-version index 18711d290eac4..5595ae1aa9e4c 100644 --- a/.node-version +++ b/.node-version @@ -1 +1 @@ -14.17.5 +14.17.6 diff --git a/.nvmrc b/.nvmrc index 18711d290eac4..5595ae1aa9e4c 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -14.17.5 +14.17.6 diff --git a/WORKSPACE.bazel b/WORKSPACE.bazel index 384277822709c..3ae3f202a3bfd 100644 --- a/WORKSPACE.bazel +++ b/WORKSPACE.bazel @@ -27,13 +27,13 @@ check_rules_nodejs_version(minimum_version_string = "3.8.0") # we can update that rule. node_repositories( node_repositories = { - "14.17.5-darwin_amd64": ("node-v14.17.5-darwin-x64.tar.gz", "node-v14.17.5-darwin-x64", "2e40ab625b45b9bdfcb963ddd4d65d87ddf1dd37a86b6f8b075cf3d77fe9dc09"), - "14.17.5-linux_arm64": ("node-v14.17.5-linux-arm64.tar.xz", "node-v14.17.5-linux-arm64", "3a2e674b6db50dfde767c427e8f077235bbf6f9236e1b12a4cc3496b12f94bae"), - "14.17.5-linux_s390x": ("node-v14.17.5-linux-s390x.tar.xz", "node-v14.17.5-linux-s390x", "7d40eee3d54241403db12fb3bc420cd776e2b02e89100c45cf5e74a73942e7f6"), - "14.17.5-linux_amd64": ("node-v14.17.5-linux-x64.tar.xz", "node-v14.17.5-linux-x64", "2d759de07a50cd7f75bd73d67e97b0d0e095ee3c413efac7d1b3d1e84ed76fff"), - "14.17.5-windows_amd64": ("node-v14.17.5-win-x64.zip", "node-v14.17.5-win-x64", "a99b7ee08e846e5d1f4e70c4396265542819d79ed9cebcc27760b89571f03cbf"), + "14.17.6-darwin_amd64": ("node-v14.17.6-darwin-x64.tar.gz", "node-v14.17.6-darwin-x64", "e3e4c02240d74fb1dc8a514daa62e5de04f7eaee0bcbca06a366ece73a52ad88"), + "14.17.6-linux_arm64": ("node-v14.17.6-linux-arm64.tar.xz", "node-v14.17.6-linux-arm64", "9c4f3a651e03cd9b5bddd33a80e8be6a6eb15e518513e410bb0852a658699156"), + "14.17.6-linux_s390x": ("node-v14.17.6-linux-s390x.tar.xz", "node-v14.17.6-linux-s390x", "3677f35b97608056013b5368f86eecdb044bdccc1b3976c1d4448736c37b6a0c"), + "14.17.6-linux_amd64": ("node-v14.17.6-linux-x64.tar.xz", "node-v14.17.6-linux-x64", "3bbe4faf356738d88b45be222bf5e858330541ff16bd0d4cfad36540c331461b"), + "14.17.6-windows_amd64": ("node-v14.17.6-win-x64.zip", "node-v14.17.6-win-x64", "b83e9ce542fda7fc519cec6eb24a2575a84862ea4227dedc171a8e0b5b614ac0"), }, - node_version = "14.17.5", + node_version = "14.17.6", node_urls = [ "https://nodejs.org/dist/v{version}/{filename}", ], diff --git a/package.json b/package.json index a5605371c2c45..20d20d13fa121 100644 --- a/package.json +++ b/package.json @@ -89,7 +89,7 @@ "**/underscore": "^1.13.1" }, "engines": { - "node": "14.17.5", + "node": "14.17.6", "yarn": "^1.21.1" }, "dependencies": { From a9e77fdfdba7be392b49a289048d21824015596c Mon Sep 17 00:00:00 2001 From: Esteban Beltran Date: Wed, 1 Sep 2021 09:04:33 +0200 Subject: [PATCH 26/81] Remove skip from flaky test. (#110494) --- .../security_solution_endpoint/apps/endpoint/policy_details.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/x-pack/test/security_solution_endpoint/apps/endpoint/policy_details.ts b/x-pack/test/security_solution_endpoint/apps/endpoint/policy_details.ts index ba046a081b6d8..06398fdcd9658 100644 --- a/x-pack/test/security_solution_endpoint/apps/endpoint/policy_details.ts +++ b/x-pack/test/security_solution_endpoint/apps/endpoint/policy_details.ts @@ -21,8 +21,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { const testSubjects = getService('testSubjects'); const policyTestResources = getService('policyTestResources'); - // FLAKY: https://github.com/elastic/kibana/issues/100296 - describe.skip('When on the Endpoint Policy Details Page', function () { + describe('When on the Endpoint Policy Details Page', function () { describe('with an invalid policy id', () => { it('should display an error', async () => { await pageObjects.policy.navigateToPolicyDetails('invalid-id'); From 16af3e95cb00d338a3fcdae45f69101110af736e Mon Sep 17 00:00:00 2001 From: Xavier Mouligneau <189600+XavierM@users.noreply.github.com> Date: Wed, 1 Sep 2021 04:23:44 -0400 Subject: [PATCH 27/81] [RAC] Remove rbac on security solution side (#110472) * wip to remove rbac * Revert "[Cases] Include rule registry client for updating alert statuses (#108588)" This reverts commit 1fd7038b34084052895bb926b80c2301e4588de9. This leaves the rule registry mock changes * remove rbac on Trend/Count alert * update detection api for status * remove @kbn-alerts packages * fix leftover * Switching cases to leverage update by query for alert status * Adding missed files * fix bad logic * updating tests for use_alerts_privileges * remove index alias/fields * fix types * fix plugin to get the right index names * left over of alis on template * forget to use current user for create/read route index * updated alerts page to not show table when no privileges and updates to tests * fix bug when switching between o11y and security solution * updates tests and move to use privileges page when user tries to access alerts without proper access * updating jest tests * pairing with yara * bring back kbn-alerts after discussion with the team * fix types * fix index field for o11y * fix bug with updating index priv state * fix i18n issue and update api docs * fix refresh on alerts * fix render view on alerts * updating tests and checking for null in alerts page to not show no privileges page before load * fix details rules Co-authored-by: Jonathan Buttner Co-authored-by: Yara Tercero --- ...-plugin-core-public.doclinksstart.links.md | 1 + ...kibana-plugin-core-public.doclinksstart.md | 2 +- .../public/doc_links/doc_links_service.ts | 2 + src/core/public/public.api.md | 1 + x-pack/plugins/cases/kibana.json | 1 - .../plugins/cases/server/client/alerts/get.ts | 14 +- .../cases/server/client/alerts/types.ts | 12 +- .../server/client/alerts/update_status.ts | 4 +- .../cases/server/client/attachments/add.ts | 34 +- .../plugins/cases/server/client/cases/push.ts | 65 ++-- .../cases/server/client/cases/update.ts | 27 +- x-pack/plugins/cases/server/client/factory.ts | 19 +- .../cases/server/client/sub_cases/update.ts | 21 +- x-pack/plugins/cases/server/client/types.ts | 3 +- .../server/common/models/commentable_case.ts | 34 +- .../connectors/servicenow/sir_format.test.ts | 159 +++------- .../connectors/servicenow/sir_format.ts | 36 +-- x-pack/plugins/cases/server/plugin.ts | 7 +- .../server/services/alerts/index.test.ts | 295 +++++++++++++++--- .../cases/server/services/alerts/index.ts | 207 +++++++----- .../cases/server/services/alerts/types.ts | 13 - .../detection_alerts/alerts_details.spec.ts | 2 +- .../security_solution/cypress/tasks/alerts.ts | 7 +- .../cypress/tasks/create_new_rule.ts | 1 - .../public/app/deep_links/index.test.ts | 64 ---- .../public/app/deep_links/index.ts | 11 +- .../cases/components/case_view/index.tsx | 2 +- .../events_viewer/events_viewer.test.tsx | 8 - .../common/components/events_viewer/index.tsx | 13 +- .../index.test.tsx | 7 - .../use_navigation_items.tsx | 12 +- .../components/user_privileges/index.tsx | 26 +- .../alerts_kpis/alerts_count_panel/index.tsx | 6 +- .../alerts_histogram_panel/index.tsx | 17 +- .../components/alerts_table/index.tsx | 4 +- .../alert_context_menu.test.tsx | 8 - .../timeline_actions/use_alerts_actions.tsx | 11 +- .../translations.tsx | 70 +++-- .../use_missing_privileges.ts | 17 +- .../take_action_dropdown/index.test.tsx | 6 +- .../components/user_info/index.test.tsx | 1 + .../detections/components/user_info/index.tsx | 56 ++-- .../alerts/use_alerts_privileges.test.tsx | 78 ++++- .../alerts/use_alerts_privileges.tsx | 25 +- .../alerts/use_signal_index.test.tsx | 7 - .../public/detections/pages/alerts/index.tsx | 15 +- .../detection_engine.test.tsx | 3 +- .../detection_engine/detection_engine.tsx | 180 ++++++----- .../rules/details/index.test.tsx | 7 - .../pages/detection_engine/translations.ts | 15 + .../public/overview/pages/overview.tsx | 5 +- .../security_solution/public/plugin.tsx | 28 +- .../components/side_panel/index.test.tsx | 8 - .../timeline/body/actions/index.test.tsx | 8 - .../body/events/event_column_view.test.tsx | 7 - .../security_solution/server/features.ts | 2 +- .../get_signals_template.test.ts.snap | 21 -- .../routes/index/create_index_route.ts | 61 ++-- .../routes/index/get_signals_template.test.ts | 13 +- .../routes/index/get_signals_template.ts | 12 +- .../routes/index/read_index_route.ts | 2 +- .../signals/open_close_signals_route.ts | 8 +- x-pack/plugins/timelines/common/constants.ts | 1 + .../common/types/timeline/actions/index.ts | 1 + .../components/t_grid/body/height_hack.ts | 10 +- .../components/t_grid/integrated/index.tsx | 19 +- .../alert_status_bulk_actions.tsx | 1 + .../timelines/public/container/index.tsx | 4 +- .../public/container/use_update_alerts.ts | 33 +- .../hooks/use_status_bulk_action_items.tsx | 3 +- .../plugins/timelines/public/mock/t_grid.tsx | 1 + x-pack/plugins/timelines/public/plugin.ts | 7 + .../search_strategy/index_fields/index.ts | 2 +- .../translations/translations/ja-JP.json | 1 - .../translations/translations/zh-CN.json | 1 - .../apis/security/privileges.ts | 11 +- .../security_and_spaces/tests/create_index.ts | 54 ++-- 77 files changed, 1071 insertions(+), 889 deletions(-) delete mode 100644 x-pack/plugins/cases/server/services/alerts/types.ts diff --git a/docs/development/core/public/kibana-plugin-core-public.doclinksstart.links.md b/docs/development/core/public/kibana-plugin-core-public.doclinksstart.links.md index cadb34ae63b86..26d0c38f72fd7 100644 --- a/docs/development/core/public/kibana-plugin-core-public.doclinksstart.links.md +++ b/docs/development/core/public/kibana-plugin-core-public.doclinksstart.links.md @@ -128,6 +128,7 @@ readonly links: { readonly rollupJobs: string; readonly elasticsearch: Record; readonly siem: { + readonly privileges: string; readonly guide: string; readonly gettingStarted: string; readonly ml: string; diff --git a/docs/development/core/public/kibana-plugin-core-public.doclinksstart.md b/docs/development/core/public/kibana-plugin-core-public.doclinksstart.md index aded69733b58b..aa3f958018041 100644 --- a/docs/development/core/public/kibana-plugin-core-public.doclinksstart.md +++ b/docs/development/core/public/kibana-plugin-core-public.doclinksstart.md @@ -17,5 +17,5 @@ export interface DocLinksStart | --- | --- | --- | | [DOC\_LINK\_VERSION](./kibana-plugin-core-public.doclinksstart.doc_link_version.md) | string | | | [ELASTIC\_WEBSITE\_URL](./kibana-plugin-core-public.doclinksstart.elastic_website_url.md) | string | | -| [links](./kibana-plugin-core-public.doclinksstart.links.md) | {
readonly settings: string;
readonly canvas: {
readonly guide: string;
};
readonly dashboard: {
readonly guide: string;
readonly drilldowns: string;
readonly drilldownsTriggerPicker: string;
readonly urlDrilldownTemplateSyntax: string;
readonly urlDrilldownVariables: string;
};
readonly discover: Record<string, string>;
readonly filebeat: {
readonly base: string;
readonly installation: string;
readonly configuration: string;
readonly elasticsearchOutput: string;
readonly elasticsearchModule: string;
readonly startup: string;
readonly exportedFields: string;
readonly suricataModule: string;
readonly zeekModule: string;
};
readonly auditbeat: {
readonly base: string;
readonly auditdModule: string;
readonly systemModule: string;
};
readonly metricbeat: {
readonly base: string;
readonly configure: string;
readonly httpEndpoint: string;
readonly install: string;
readonly start: string;
};
readonly enterpriseSearch: {
readonly base: string;
readonly appSearchBase: string;
readonly workplaceSearchBase: string;
};
readonly heartbeat: {
readonly base: string;
};
readonly libbeat: {
readonly getStarted: string;
};
readonly logstash: {
readonly base: string;
};
readonly functionbeat: {
readonly base: string;
};
readonly winlogbeat: {
readonly base: string;
};
readonly aggs: {
readonly composite: string;
readonly composite_missing_bucket: string;
readonly date_histogram: string;
readonly date_range: string;
readonly date_format_pattern: string;
readonly filter: string;
readonly filters: string;
readonly geohash_grid: string;
readonly histogram: string;
readonly ip_range: string;
readonly range: string;
readonly significant_terms: string;
readonly terms: string;
readonly avg: string;
readonly avg_bucket: string;
readonly max_bucket: string;
readonly min_bucket: string;
readonly sum_bucket: string;
readonly cardinality: string;
readonly count: string;
readonly cumulative_sum: string;
readonly derivative: string;
readonly geo_bounds: string;
readonly geo_centroid: string;
readonly max: string;
readonly median: string;
readonly min: string;
readonly moving_avg: string;
readonly percentile_ranks: string;
readonly serial_diff: string;
readonly std_dev: string;
readonly sum: string;
readonly top_hits: string;
};
readonly runtimeFields: {
readonly overview: string;
readonly mapping: string;
};
readonly scriptedFields: {
readonly scriptFields: string;
readonly scriptAggs: string;
readonly painless: string;
readonly painlessApi: string;
readonly painlessLangSpec: string;
readonly painlessSyntax: string;
readonly painlessWalkthrough: string;
readonly luceneExpressions: string;
};
readonly search: {
readonly sessions: string;
readonly sessionLimits: string;
};
readonly indexPatterns: {
readonly introduction: string;
readonly fieldFormattersNumber: string;
readonly fieldFormattersString: string;
readonly runtimeFields: string;
};
readonly addData: string;
readonly kibana: string;
readonly upgradeAssistant: string;
readonly rollupJobs: string;
readonly elasticsearch: Record<string, string>;
readonly siem: {
readonly guide: string;
readonly gettingStarted: string;
readonly ml: string;
readonly ruleChangeLog: string;
readonly detectionsReq: string;
readonly networkMap: string;
};
readonly query: {
readonly eql: string;
readonly kueryQuerySyntax: string;
readonly luceneQuerySyntax: string;
readonly percolate: string;
readonly queryDsl: string;
readonly autocompleteChanges: string;
};
readonly date: {
readonly dateMath: string;
readonly dateMathIndexNames: string;
};
readonly management: Record<string, string>;
readonly ml: Record<string, string>;
readonly transforms: Record<string, string>;
readonly visualize: Record<string, string>;
readonly apis: Readonly<{
bulkIndexAlias: string;
byteSizeUnits: string;
createAutoFollowPattern: string;
createFollower: string;
createIndex: string;
createSnapshotLifecyclePolicy: string;
createRoleMapping: string;
createRoleMappingTemplates: string;
createRollupJobsRequest: string;
createApiKey: string;
createPipeline: string;
createTransformRequest: string;
cronExpressions: string;
executeWatchActionModes: string;
indexExists: string;
openIndex: string;
putComponentTemplate: string;
painlessExecute: string;
painlessExecuteAPIContexts: string;
putComponentTemplateMetadata: string;
putSnapshotLifecyclePolicy: string;
putIndexTemplateV1: string;
putWatch: string;
simulatePipeline: string;
timeUnits: string;
updateTransform: string;
}>;
readonly observability: Record<string, string>;
readonly alerting: Record<string, string>;
readonly maps: Record<string, string>;
readonly monitoring: Record<string, string>;
readonly security: Readonly<{
apiKeyServiceSettings: string;
clusterPrivileges: string;
elasticsearchSettings: string;
elasticsearchEnableSecurity: string;
indicesPrivileges: string;
kibanaTLS: string;
kibanaPrivileges: string;
mappingRoles: string;
mappingRolesFieldRules: string;
runAsPrivilege: string;
}>;
readonly watcher: Record<string, string>;
readonly ccs: Record<string, string>;
readonly plugins: Record<string, string>;
readonly snapshotRestore: Record<string, string>;
readonly ingest: Record<string, string>;
readonly fleet: Readonly<{
guide: string;
fleetServer: string;
fleetServerAddFleetServer: string;
settings: string;
settingsFleetServerHostSettings: string;
troubleshooting: string;
elasticAgent: string;
datastreams: string;
datastreamsNamingScheme: string;
upgradeElasticAgent: string;
upgradeElasticAgent712lower: string;
}>;
readonly ecs: {
readonly guide: string;
};
} | | +| [links](./kibana-plugin-core-public.doclinksstart.links.md) | {
readonly settings: string;
readonly canvas: {
readonly guide: string;
};
readonly dashboard: {
readonly guide: string;
readonly drilldowns: string;
readonly drilldownsTriggerPicker: string;
readonly urlDrilldownTemplateSyntax: string;
readonly urlDrilldownVariables: string;
};
readonly discover: Record<string, string>;
readonly filebeat: {
readonly base: string;
readonly installation: string;
readonly configuration: string;
readonly elasticsearchOutput: string;
readonly elasticsearchModule: string;
readonly startup: string;
readonly exportedFields: string;
readonly suricataModule: string;
readonly zeekModule: string;
};
readonly auditbeat: {
readonly base: string;
readonly auditdModule: string;
readonly systemModule: string;
};
readonly metricbeat: {
readonly base: string;
readonly configure: string;
readonly httpEndpoint: string;
readonly install: string;
readonly start: string;
};
readonly enterpriseSearch: {
readonly base: string;
readonly appSearchBase: string;
readonly workplaceSearchBase: string;
};
readonly heartbeat: {
readonly base: string;
};
readonly libbeat: {
readonly getStarted: string;
};
readonly logstash: {
readonly base: string;
};
readonly functionbeat: {
readonly base: string;
};
readonly winlogbeat: {
readonly base: string;
};
readonly aggs: {
readonly composite: string;
readonly composite_missing_bucket: string;
readonly date_histogram: string;
readonly date_range: string;
readonly date_format_pattern: string;
readonly filter: string;
readonly filters: string;
readonly geohash_grid: string;
readonly histogram: string;
readonly ip_range: string;
readonly range: string;
readonly significant_terms: string;
readonly terms: string;
readonly avg: string;
readonly avg_bucket: string;
readonly max_bucket: string;
readonly min_bucket: string;
readonly sum_bucket: string;
readonly cardinality: string;
readonly count: string;
readonly cumulative_sum: string;
readonly derivative: string;
readonly geo_bounds: string;
readonly geo_centroid: string;
readonly max: string;
readonly median: string;
readonly min: string;
readonly moving_avg: string;
readonly percentile_ranks: string;
readonly serial_diff: string;
readonly std_dev: string;
readonly sum: string;
readonly top_hits: string;
};
readonly runtimeFields: {
readonly overview: string;
readonly mapping: string;
};
readonly scriptedFields: {
readonly scriptFields: string;
readonly scriptAggs: string;
readonly painless: string;
readonly painlessApi: string;
readonly painlessLangSpec: string;
readonly painlessSyntax: string;
readonly painlessWalkthrough: string;
readonly luceneExpressions: string;
};
readonly search: {
readonly sessions: string;
readonly sessionLimits: string;
};
readonly indexPatterns: {
readonly introduction: string;
readonly fieldFormattersNumber: string;
readonly fieldFormattersString: string;
readonly runtimeFields: string;
};
readonly addData: string;
readonly kibana: string;
readonly upgradeAssistant: string;
readonly rollupJobs: string;
readonly elasticsearch: Record<string, string>;
readonly siem: {
readonly privileges: string;
readonly guide: string;
readonly gettingStarted: string;
readonly ml: string;
readonly ruleChangeLog: string;
readonly detectionsReq: string;
readonly networkMap: string;
};
readonly query: {
readonly eql: string;
readonly kueryQuerySyntax: string;
readonly luceneQuerySyntax: string;
readonly percolate: string;
readonly queryDsl: string;
readonly autocompleteChanges: string;
};
readonly date: {
readonly dateMath: string;
readonly dateMathIndexNames: string;
};
readonly management: Record<string, string>;
readonly ml: Record<string, string>;
readonly transforms: Record<string, string>;
readonly visualize: Record<string, string>;
readonly apis: Readonly<{
bulkIndexAlias: string;
byteSizeUnits: string;
createAutoFollowPattern: string;
createFollower: string;
createIndex: string;
createSnapshotLifecyclePolicy: string;
createRoleMapping: string;
createRoleMappingTemplates: string;
createRollupJobsRequest: string;
createApiKey: string;
createPipeline: string;
createTransformRequest: string;
cronExpressions: string;
executeWatchActionModes: string;
indexExists: string;
openIndex: string;
putComponentTemplate: string;
painlessExecute: string;
painlessExecuteAPIContexts: string;
putComponentTemplateMetadata: string;
putSnapshotLifecyclePolicy: string;
putIndexTemplateV1: string;
putWatch: string;
simulatePipeline: string;
timeUnits: string;
updateTransform: string;
}>;
readonly observability: Record<string, string>;
readonly alerting: Record<string, string>;
readonly maps: Record<string, string>;
readonly monitoring: Record<string, string>;
readonly security: Readonly<{
apiKeyServiceSettings: string;
clusterPrivileges: string;
elasticsearchSettings: string;
elasticsearchEnableSecurity: string;
indicesPrivileges: string;
kibanaTLS: string;
kibanaPrivileges: string;
mappingRoles: string;
mappingRolesFieldRules: string;
runAsPrivilege: string;
}>;
readonly watcher: Record<string, string>;
readonly ccs: Record<string, string>;
readonly plugins: Record<string, string>;
readonly snapshotRestore: Record<string, string>;
readonly ingest: Record<string, string>;
readonly fleet: Readonly<{
guide: string;
fleetServer: string;
fleetServerAddFleetServer: string;
settings: string;
settingsFleetServerHostSettings: string;
troubleshooting: string;
elasticAgent: string;
datastreams: string;
datastreamsNamingScheme: string;
upgradeElasticAgent: string;
upgradeElasticAgent712lower: string;
}>;
readonly ecs: {
readonly guide: string;
};
} | | diff --git a/src/core/public/doc_links/doc_links_service.ts b/src/core/public/doc_links/doc_links_service.ts index 4b1aaf9eb19c1..9ff95c0e04d17 100644 --- a/src/core/public/doc_links/doc_links_service.ts +++ b/src/core/public/doc_links/doc_links_service.ts @@ -204,6 +204,7 @@ export class DocLinksService { siem: { guide: `${ELASTIC_WEBSITE_URL}guide/en/security/${DOC_LINK_VERSION}/index.html`, gettingStarted: `${ELASTIC_WEBSITE_URL}guide/en/security/${DOC_LINK_VERSION}/index.html`, + privileges: `${ELASTIC_WEBSITE_URL}guide/en/security/${DOC_LINK_VERSION}/sec-requirements.html`, ml: `${ELASTIC_WEBSITE_URL}guide/en/security/${DOC_LINK_VERSION}/machine-learning.html`, ruleChangeLog: `${ELASTIC_WEBSITE_URL}guide/en/security/${DOC_LINK_VERSION}/prebuilt-rules-changelog.html`, detectionsReq: `${ELASTIC_WEBSITE_URL}guide/en/security/${DOC_LINK_VERSION}/detections-permissions-section.html`, @@ -569,6 +570,7 @@ export interface DocLinksStart { readonly rollupJobs: string; readonly elasticsearch: Record; readonly siem: { + readonly privileges: string; readonly guide: string; readonly gettingStarted: string; readonly ml: string; diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index 043759378faa3..3a432ae50ea7d 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -592,6 +592,7 @@ export interface DocLinksStart { readonly rollupJobs: string; readonly elasticsearch: Record; readonly siem: { + readonly privileges: string; readonly guide: string; readonly gettingStarted: string; readonly ml: string; diff --git a/x-pack/plugins/cases/kibana.json b/x-pack/plugins/cases/kibana.json index 3889c559238b3..ebac6295166df 100644 --- a/x-pack/plugins/cases/kibana.json +++ b/x-pack/plugins/cases/kibana.json @@ -10,7 +10,6 @@ "id":"cases", "kibanaVersion":"kibana", "optionalPlugins":[ - "ruleRegistry", "security", "spaces" ], diff --git a/x-pack/plugins/cases/server/client/alerts/get.ts b/x-pack/plugins/cases/server/client/alerts/get.ts index 391279aab5a83..2048ccae4fa60 100644 --- a/x-pack/plugins/cases/server/client/alerts/get.ts +++ b/x-pack/plugins/cases/server/client/alerts/get.ts @@ -12,11 +12,19 @@ export const get = async ( { alertsInfo }: AlertGet, clientArgs: CasesClientArgs ): Promise => { - const { alertsService, logger } = clientArgs; + const { alertsService, scopedClusterClient, logger } = clientArgs; if (alertsInfo.length === 0) { return []; } - const alerts = await alertsService.getAlerts({ alertsInfo, logger }); - return alerts ?? []; + const alerts = await alertsService.getAlerts({ alertsInfo, scopedClusterClient, logger }); + if (!alerts) { + return []; + } + + return alerts.docs.map((alert) => ({ + id: alert._id, + index: alert._index, + ...alert._source, + })); }; diff --git a/x-pack/plugins/cases/server/client/alerts/types.ts b/x-pack/plugins/cases/server/client/alerts/types.ts index 6b3a49f20d1e5..95cd9ae33bff9 100644 --- a/x-pack/plugins/cases/server/client/alerts/types.ts +++ b/x-pack/plugins/cases/server/client/alerts/types.ts @@ -7,7 +7,17 @@ import { CaseStatuses } from '../../../common/api'; import { AlertInfo } from '../../common'; -import { Alert } from '../../services/alerts/types'; + +interface Alert { + id: string; + index: string; + destination?: { + ip: string; + }; + source?: { + ip: string; + }; +} export type CasesClientGetAlertsResponse = Alert[]; diff --git a/x-pack/plugins/cases/server/client/alerts/update_status.ts b/x-pack/plugins/cases/server/client/alerts/update_status.ts index 9c8cc33264413..a0684b59241b0 100644 --- a/x-pack/plugins/cases/server/client/alerts/update_status.ts +++ b/x-pack/plugins/cases/server/client/alerts/update_status.ts @@ -16,6 +16,6 @@ export const updateStatus = async ( { alerts }: UpdateAlertsStatusArgs, clientArgs: CasesClientArgs ): Promise => { - const { alertsService, logger } = clientArgs; - await alertsService.updateAlertsStatus({ alerts, logger }); + const { alertsService, scopedClusterClient, logger } = clientArgs; + await alertsService.updateAlertsStatus({ alerts, scopedClusterClient, logger }); }; diff --git a/x-pack/plugins/cases/server/client/attachments/add.ts b/x-pack/plugins/cases/server/client/attachments/add.ts index 5393a108d6af2..166ae2ae65012 100644 --- a/x-pack/plugins/cases/server/client/attachments/add.ts +++ b/x-pack/plugins/cases/server/client/attachments/add.ts @@ -40,7 +40,12 @@ import { } from '../../services/user_actions/helpers'; import { AttachmentService, CasesService, CaseUserActionService } from '../../services'; -import { createCaseError, CommentableCase, isCommentRequestTypeGenAlert } from '../../common'; +import { + createCaseError, + CommentableCase, + createAlertUpdateRequest, + isCommentRequestTypeGenAlert, +} from '../../common'; import { CasesClientArgs, CasesClientInternal } from '..'; import { decodeCommentRequest } from '../utils'; @@ -190,9 +195,22 @@ const addGeneratedAlerts = async ( user: userDetails, commentReq: query, id: savedObjectID, - casesClientInternal, }); + if ( + (newComment.attributes.type === CommentType.alert || + newComment.attributes.type === CommentType.generatedAlert) && + caseInfo.attributes.settings.syncAlerts + ) { + const alertsToUpdate = createAlertUpdateRequest({ + comment: query, + status: subCase.attributes.status, + }); + await casesClientInternal.alerts.updateStatus({ + alerts: alertsToUpdate, + }); + } + await userActionService.bulkCreate({ unsecuredSavedObjectsClient, actions: [ @@ -368,9 +386,19 @@ export const addComment = async ( user: userInfo, commentReq: query, id: savedObjectID, - casesClientInternal, }); + if (newComment.attributes.type === CommentType.alert && updatedCase.settings.syncAlerts) { + const alertsToUpdate = createAlertUpdateRequest({ + comment: query, + status: updatedCase.status, + }); + + await casesClientInternal.alerts.updateStatus({ + alerts: alertsToUpdate, + }); + } + await userActionService.bulkCreate({ unsecuredSavedObjectsClient, actions: [ diff --git a/x-pack/plugins/cases/server/client/cases/push.ts b/x-pack/plugins/cases/server/client/cases/push.ts index 80e69d53e9e8b..3048cf01bb3ba 100644 --- a/x-pack/plugins/cases/server/client/cases/push.ts +++ b/x-pack/plugins/cases/server/client/cases/push.ts @@ -6,7 +6,7 @@ */ import Boom from '@hapi/boom'; -import { SavedObjectsFindResponse, SavedObject, Logger } from 'kibana/server'; +import { SavedObjectsFindResponse, SavedObject } from 'kibana/server'; import { ActionConnector, @@ -22,16 +22,26 @@ import { import { buildCaseUserActionItem } from '../../services/user_actions/helpers'; import { createIncident, getCommentContextFromAttributes } from './utils'; -import { - AlertInfo, - createCaseError, - flattenCaseSavedObject, - getAlertInfoFromComments, -} from '../../common'; +import { createCaseError, flattenCaseSavedObject, getAlertInfoFromComments } from '../../common'; import { CasesClient, CasesClientArgs, CasesClientInternal } from '..'; import { Operations } from '../../authorization'; import { casesConnectors } from '../../connectors'; -import { CasesClientGetAlertsResponse } from '../alerts/types'; + +/** + * Returns true if the case should be closed based on the configuration settings and whether the case + * is a collection. Collections are not closable because we aren't allowing their status to be changed. + * In the future we could allow push to close all the sub cases of a collection but that's not currently supported. + */ +function shouldCloseByPush( + configureSettings: SavedObjectsFindResponse, + caseInfo: SavedObject +): boolean { + return ( + configureSettings.total > 0 && + configureSettings.saved_objects[0].attributes.closure_type === 'close-by-pushing' && + caseInfo.attributes.type !== CaseType.collection + ); +} /** * Parameters for pushing a case to an external system @@ -96,7 +106,9 @@ export const push = async ( const alertsInfo = getAlertInfoFromComments(theCase?.comments); - const alerts = await getAlertsCatchErrors({ casesClientInternal, alertsInfo, logger }); + const alerts = await casesClientInternal.alerts.get({ + alertsInfo, + }); const getMappingsResponse = await casesClientInternal.configuration.getMappings({ connector: theCase.connector, @@ -266,38 +278,3 @@ export const push = async ( throw createCaseError({ message: `Failed to push case: ${error}`, error, logger }); } }; - -async function getAlertsCatchErrors({ - casesClientInternal, - alertsInfo, - logger, -}: { - casesClientInternal: CasesClientInternal; - alertsInfo: AlertInfo[]; - logger: Logger; -}): Promise { - try { - return await casesClientInternal.alerts.get({ - alertsInfo, - }); - } catch (error) { - logger.error(`Failed to retrieve alerts during push: ${error}`); - return []; - } -} - -/** - * Returns true if the case should be closed based on the configuration settings and whether the case - * is a collection. Collections are not closable because we aren't allowing their status to be changed. - * In the future we could allow push to close all the sub cases of a collection but that's not currently supported. - */ -function shouldCloseByPush( - configureSettings: SavedObjectsFindResponse, - caseInfo: SavedObject -): boolean { - return ( - configureSettings.total > 0 && - configureSettings.saved_objects[0].attributes.closure_type === 'close-by-pushing' && - caseInfo.attributes.type !== CaseType.collection - ); -} diff --git a/x-pack/plugins/cases/server/client/cases/update.ts b/x-pack/plugins/cases/server/client/cases/update.ts index 611c9e09fa76e..ed19444414d57 100644 --- a/x-pack/plugins/cases/server/client/cases/update.ts +++ b/x-pack/plugins/cases/server/client/cases/update.ts @@ -12,7 +12,6 @@ import { fold } from 'fp-ts/lib/Either'; import { identity } from 'fp-ts/lib/function'; import { - Logger, SavedObject, SavedObjectsClientContract, SavedObjectsFindResponse, @@ -308,14 +307,12 @@ async function updateAlerts({ caseService, unsecuredSavedObjectsClient, casesClientInternal, - logger, }: { casesWithSyncSettingChangedToOn: UpdateRequestWithOriginalCase[]; casesWithStatusChangedAndSynced: UpdateRequestWithOriginalCase[]; caseService: CasesService; unsecuredSavedObjectsClient: SavedObjectsClientContract; casesClientInternal: CasesClientInternal; - logger: Logger; }) { /** * It's possible that a case ID can appear multiple times in each array. I'm intentionally placing the status changes @@ -364,9 +361,7 @@ async function updateAlerts({ [] ); - await casesClientInternal.alerts.updateStatus({ - alerts: alertsToUpdate, - }); + await casesClientInternal.alerts.updateStatus({ alerts: alertsToUpdate }); } function partitionPatchRequest( @@ -567,6 +562,15 @@ export const update = async ( ); }); + // Update the alert's status to match any case status or sync settings changes + await updateAlerts({ + casesWithStatusChangedAndSynced, + casesWithSyncSettingChangedToOn, + caseService, + unsecuredSavedObjectsClient, + casesClientInternal, + }); + const returnUpdatedCase = myCases.saved_objects .filter((myCase) => updatedCases.saved_objects.some((updatedCase) => updatedCase.id === myCase.id) @@ -594,17 +598,6 @@ export const update = async ( }), }); - // Update the alert's status to match any case status or sync settings changes - // Attempt to do this after creating/changing the other entities just in case it fails - await updateAlerts({ - casesWithStatusChangedAndSynced, - casesWithSyncSettingChangedToOn, - caseService, - unsecuredSavedObjectsClient, - casesClientInternal, - logger, - }); - return CasesResponseRt.encode(returnUpdatedCase); } catch (error) { const idVersions = cases.cases.map((caseInfo) => ({ diff --git a/x-pack/plugins/cases/server/client/factory.ts b/x-pack/plugins/cases/server/client/factory.ts index a1a3ccdd3bc52..2fae6996f4aa2 100644 --- a/x-pack/plugins/cases/server/client/factory.ts +++ b/x-pack/plugins/cases/server/client/factory.ts @@ -5,7 +5,12 @@ * 2.0. */ -import { KibanaRequest, SavedObjectsServiceStart, Logger } from 'kibana/server'; +import { + KibanaRequest, + SavedObjectsServiceStart, + Logger, + ElasticsearchClient, +} from 'kibana/server'; import { SecurityPluginSetup, SecurityPluginStart } from '../../../security/server'; import { SAVED_OBJECT_TYPES } from '../../common'; import { Authorization } from '../authorization/authorization'; @@ -20,8 +25,8 @@ import { } from '../services'; import { PluginStartContract as FeaturesPluginStart } from '../../../features/server'; import { PluginStartContract as ActionsPluginStart } from '../../../actions/server'; -import { RuleRegistryPluginStartContract } from '../../../rule_registry/server'; import { LensServerPluginSetup } from '../../../lens/server'; + import { AuthorizationAuditLogger } from '../authorization'; import { CasesClient, createCasesClient } from '.'; @@ -31,7 +36,6 @@ interface CasesClientFactoryArgs { getSpace: GetSpaceFn; featuresPluginStart: FeaturesPluginStart; actionsPluginStart: ActionsPluginStart; - ruleRegistryPluginStart?: RuleRegistryPluginStartContract; lensEmbeddableFactory: LensServerPluginSetup['lensEmbeddableFactory']; } @@ -65,10 +69,12 @@ export class CasesClientFactory { */ public async create({ request, + scopedClusterClient, savedObjectsService, }: { request: KibanaRequest; savedObjectsService: SavedObjectsServiceStart; + scopedClusterClient: ElasticsearchClient; }): Promise { if (!this.isInitialized || !this.options) { throw new Error('CasesClientFactory must be initialized before calling create'); @@ -88,12 +94,9 @@ export class CasesClientFactory { const caseService = new CasesService(this.logger, this.options?.securityPluginStart?.authc); const userInfo = caseService.getUser({ request }); - const alertsClient = await this.options.ruleRegistryPluginStart?.getRacClientWithRequest( - request - ); - return createCasesClient({ - alertsService: new AlertService(alertsClient), + alertsService: new AlertService(), + scopedClusterClient, unsecuredSavedObjectsClient: savedObjectsService.getScopedClient(request, { includedHiddenTypes: SAVED_OBJECT_TYPES, // this tells the security plugin to not perform SO authorization and audit logging since we are handling diff --git a/x-pack/plugins/cases/server/client/sub_cases/update.ts b/x-pack/plugins/cases/server/client/sub_cases/update.ts index 56610ea6858e3..c8cb96cbb6b8c 100644 --- a/x-pack/plugins/cases/server/client/sub_cases/update.ts +++ b/x-pack/plugins/cases/server/client/sub_cases/update.ts @@ -246,9 +246,7 @@ async function updateAlerts({ [] ); - await casesClientInternal.alerts.updateStatus({ - alerts: alertsToUpdate, - }); + await casesClientInternal.alerts.updateStatus({ alerts: alertsToUpdate }); } catch (error) { throw createCaseError({ message: `Failed to update alert status while updating sub cases: ${JSON.stringify( @@ -357,6 +355,14 @@ export async function update({ ); }); + await updateAlerts({ + caseService, + unsecuredSavedObjectsClient, + casesClientInternal, + subCasesToSync: subCasesToSyncAlertsFor, + logger: clientArgs.logger, + }); + const returnUpdatedSubCases = updatedCases.saved_objects.reduce( (acc, updatedSO) => { const originalSubCase = subCasesMap.get(updatedSO.id); @@ -388,15 +394,6 @@ export async function update({ }), }); - // attempt to update the status of the alerts after creating all the user actions just in case it fails - await updateAlerts({ - caseService, - unsecuredSavedObjectsClient, - casesClientInternal, - subCasesToSync: subCasesToSyncAlertsFor, - logger: clientArgs.logger, - }); - return SubCasesResponseRt.encode(returnUpdatedSubCases); } catch (error) { const idVersions = query.subCases.map((subCase) => ({ diff --git a/x-pack/plugins/cases/server/client/types.ts b/x-pack/plugins/cases/server/client/types.ts index 3979c19949d9a..27829d2539c7d 100644 --- a/x-pack/plugins/cases/server/client/types.ts +++ b/x-pack/plugins/cases/server/client/types.ts @@ -6,7 +6,7 @@ */ import type { PublicMethodsOf } from '@kbn/utility-types'; -import { SavedObjectsClientContract, Logger } from 'kibana/server'; +import { ElasticsearchClient, SavedObjectsClientContract, Logger } from 'kibana/server'; import { User } from '../../common'; import { Authorization } from '../authorization/authorization'; import { @@ -24,6 +24,7 @@ import { LensServerPluginSetup } from '../../../lens/server'; * Parameters for initializing a cases client */ export interface CasesClientArgs { + readonly scopedClusterClient: ElasticsearchClient; readonly caseConfigureService: CaseConfigureService; readonly caseService: CasesService; readonly connectorMappingsService: ConnectorMappingsService; diff --git a/x-pack/plugins/cases/server/common/models/commentable_case.ts b/x-pack/plugins/cases/server/common/models/commentable_case.ts index e540332b1ff84..856d6378d5900 100644 --- a/x-pack/plugins/cases/server/common/models/commentable_case.ts +++ b/x-pack/plugins/cases/server/common/models/commentable_case.ts @@ -34,16 +34,10 @@ import { CommentRequestUserType, CaseAttributes, } from '../../../common'; -import { - createAlertUpdateRequest, - flattenCommentSavedObjects, - flattenSubCaseSavedObject, - transformNewComment, -} from '..'; +import { flattenCommentSavedObjects, flattenSubCaseSavedObject, transformNewComment } from '..'; import { AttachmentService, CasesService } from '../../services'; import { createCaseError } from '../error'; import { countAlertsForID } from '../index'; -import { CasesClientInternal } from '../../client'; import { getOrUpdateLensReferences } from '../utils'; interface UpdateCommentResp { @@ -279,13 +273,11 @@ export class CommentableCase { user, commentReq, id, - casesClientInternal, }: { createdDate: string; user: User; commentReq: CommentRequest; id: string; - casesClientInternal: CasesClientInternal; }): Promise { try { if (commentReq.type === CommentType.alert) { @@ -302,10 +294,6 @@ export class CommentableCase { throw Boom.badRequest('The owner field of the comment must match the case'); } - // Let's try to sync the alert's status before creating the attachment, that way if the alert doesn't exist - // we'll throw an error early before creating the attachment - await this.syncAlertStatus(commentReq, casesClientInternal); - let references = this.buildRefsToCase(); if (commentReq.type === CommentType.user && commentReq?.comment) { @@ -343,26 +331,6 @@ export class CommentableCase { } } - private async syncAlertStatus( - commentRequest: CommentRequest, - casesClientInternal: CasesClientInternal - ) { - if ( - (commentRequest.type === CommentType.alert || - commentRequest.type === CommentType.generatedAlert) && - this.settings.syncAlerts - ) { - const alertsToUpdate = createAlertUpdateRequest({ - comment: commentRequest, - status: this.status, - }); - - await casesClientInternal.alerts.updateStatus({ - alerts: alertsToUpdate, - }); - } - } - private formatCollectionForEncoding(totalComment: number) { return { id: this.collection.id, diff --git a/x-pack/plugins/cases/server/connectors/servicenow/sir_format.test.ts b/x-pack/plugins/cases/server/connectors/servicenow/sir_format.test.ts index 7a1efe8b366d0..fa103d4c1142d 100644 --- a/x-pack/plugins/cases/server/connectors/servicenow/sir_format.test.ts +++ b/x-pack/plugins/cases/server/connectors/servicenow/sir_format.test.ts @@ -24,7 +24,7 @@ describe('ITSM formatter', () => { } as CaseResponse; it('it formats correctly without alerts', async () => { - const res = format(theCase, []); + const res = await format(theCase, []); expect(res).toEqual({ dest_ip: null, source_ip: null, @@ -38,7 +38,7 @@ describe('ITSM formatter', () => { it('it formats correctly when fields do not exist ', async () => { const invalidFields = { connector: { fields: null } } as CaseResponse; - const res = format(invalidFields, []); + const res = await format(invalidFields, []); expect(res).toEqual({ dest_ip: null, source_ip: null, @@ -55,31 +55,25 @@ describe('ITSM formatter', () => { { id: 'alert-1', index: 'index-1', - source: { - destination: { ip: '192.168.1.1' }, - source: { ip: '192.168.1.2' }, - file: { - hash: { sha256: '9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08' }, - }, - url: { full: 'https://attack.com' }, + destination: { ip: '192.168.1.1' }, + source: { ip: '192.168.1.2' }, + file: { + hash: { sha256: '9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08' }, }, + url: { full: 'https://attack.com' }, }, { id: 'alert-2', index: 'index-2', - source: { - source: { - ip: '192.168.1.3', - }, - destination: { ip: '192.168.1.4' }, - file: { - hash: { sha256: '60303ae22b998861bce3b28f33eec1be758a213c86c93c076dbe9f558c11c752' }, - }, - url: { full: 'https://attack.com/api' }, + destination: { ip: '192.168.1.4' }, + source: { ip: '192.168.1.3' }, + file: { + hash: { sha256: '60303ae22b998861bce3b28f33eec1be758a213c86c93c076dbe9f558c11c752' }, }, + url: { full: 'https://attack.com/api' }, }, ]; - const res = format(theCase, alerts); + const res = await format(theCase, alerts); expect(res).toEqual({ dest_ip: '192.168.1.1,192.168.1.4', source_ip: '192.168.1.2,192.168.1.3', @@ -92,109 +86,30 @@ describe('ITSM formatter', () => { }); }); - it('it ignores alerts with an error', async () => { - const alerts = [ - { - id: 'alert-1', - index: 'index-1', - error: new Error('an error'), - source: { - destination: { ip: '192.168.1.1' }, - source: { ip: '192.168.1.2' }, - file: { - hash: { sha256: '9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08' }, - }, - url: { full: 'https://attack.com' }, - }, - }, - { - id: 'alert-2', - index: 'index-2', - source: { - source: { - ip: '192.168.1.3', - }, - destination: { ip: '192.168.1.4' }, - file: { - hash: { sha256: '60303ae22b998861bce3b28f33eec1be758a213c86c93c076dbe9f558c11c752' }, - }, - url: { full: 'https://attack.com/api' }, - }, - }, - ]; - const res = format(theCase, alerts); - expect(res).toEqual({ - dest_ip: '192.168.1.4', - source_ip: '192.168.1.3', - category: 'Denial of Service', - subcategory: 'Inbound DDos', - malware_hash: '60303ae22b998861bce3b28f33eec1be758a213c86c93c076dbe9f558c11c752', - malware_url: 'https://attack.com/api', - priority: '2 - High', - }); - }); - - it('it ignores alerts without a source field', async () => { - const alerts = [ - { - id: 'alert-1', - index: 'index-1', - }, - { - id: 'alert-2', - index: 'index-2', - source: { - source: { - ip: '192.168.1.3', - }, - destination: { ip: '192.168.1.4' }, - file: { - hash: { sha256: '60303ae22b998861bce3b28f33eec1be758a213c86c93c076dbe9f558c11c752' }, - }, - url: { full: 'https://attack.com/api' }, - }, - }, - ]; - const res = format(theCase, alerts); - expect(res).toEqual({ - dest_ip: '192.168.1.4', - source_ip: '192.168.1.3', - category: 'Denial of Service', - subcategory: 'Inbound DDos', - malware_hash: '60303ae22b998861bce3b28f33eec1be758a213c86c93c076dbe9f558c11c752', - malware_url: 'https://attack.com/api', - priority: '2 - High', - }); - }); - it('it handles duplicates correctly', async () => { const alerts = [ { id: 'alert-1', index: 'index-1', - source: { - destination: { ip: '192.168.1.1' }, - source: { ip: '192.168.1.2' }, - file: { - hash: { sha256: '9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08' }, - }, - url: { full: 'https://attack.com' }, + destination: { ip: '192.168.1.1' }, + source: { ip: '192.168.1.2' }, + file: { + hash: { sha256: '9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08' }, }, + url: { full: 'https://attack.com' }, }, { id: 'alert-2', index: 'index-2', - source: { - destination: { ip: '192.168.1.1' }, - source: { ip: '192.168.1.3' }, - file: { - hash: { sha256: '9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08' }, - }, - url: { full: 'https://attack.com/api' }, + destination: { ip: '192.168.1.1' }, + source: { ip: '192.168.1.3' }, + file: { + hash: { sha256: '9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08' }, }, + url: { full: 'https://attack.com/api' }, }, ]; - const res = format(theCase, alerts); + const res = await format(theCase, alerts); expect(res).toEqual({ dest_ip: '192.168.1.1', source_ip: '192.168.1.2,192.168.1.3', @@ -211,26 +126,22 @@ describe('ITSM formatter', () => { { id: 'alert-1', index: 'index-1', - source: { - destination: { ip: '192.168.1.1' }, - source: { ip: '192.168.1.2' }, - file: { - hash: { sha256: '9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08' }, - }, - url: { full: 'https://attack.com' }, + destination: { ip: '192.168.1.1' }, + source: { ip: '192.168.1.2' }, + file: { + hash: { sha256: '9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08' }, }, + url: { full: 'https://attack.com' }, }, { id: 'alert-2', index: 'index-2', - source: { - destination: { ip: '192.168.1.1' }, - source: { ip: '192.168.1.3' }, - file: { - hash: { sha256: '9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08' }, - }, - url: { full: 'https://attack.com/api' }, + destination: { ip: '192.168.1.1' }, + source: { ip: '192.168.1.3' }, + file: { + hash: { sha256: '9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08' }, }, + url: { full: 'https://attack.com/api' }, }, ]; @@ -239,7 +150,7 @@ describe('ITSM formatter', () => { connector: { fields: { ...theCase.connector.fields, destIp: false, malwareHash: false } }, } as CaseResponse; - const res = format(newCase, alerts); + const res = await format(newCase, alerts); expect(res).toEqual({ dest_ip: null, source_ip: '192.168.1.2,192.168.1.3', diff --git a/x-pack/plugins/cases/server/connectors/servicenow/sir_format.ts b/x-pack/plugins/cases/server/connectors/servicenow/sir_format.ts index 88b8f79d3ba5b..b48a1b7f734c8 100644 --- a/x-pack/plugins/cases/server/connectors/servicenow/sir_format.ts +++ b/x-pack/plugins/cases/server/connectors/servicenow/sir_format.ts @@ -44,25 +44,23 @@ export const format: ServiceNowSIRFormat = (theCase, alerts) => { ); if (fieldsToAdd.length > 0) { - sirFields = alerts - .filter((alert) => !alert.error && alert.source != null) - .reduce>((acc, alert) => { - fieldsToAdd.forEach((alertField) => { - const field = get(alertFieldMapping[alertField].alertPath, alert.source); - if (field && !manageDuplicate[alertFieldMapping[alertField].sirFieldKey].has(field)) { - manageDuplicate[alertFieldMapping[alertField].sirFieldKey].add(field); - acc = { - ...acc, - [alertFieldMapping[alertField].sirFieldKey]: `${ - acc[alertFieldMapping[alertField].sirFieldKey] != null - ? `${acc[alertFieldMapping[alertField].sirFieldKey]},${field}` - : field - }`, - }; - } - }); - return acc; - }, sirFields); + sirFields = alerts.reduce>((acc, alert) => { + fieldsToAdd.forEach((alertField) => { + const field = get(alertFieldMapping[alertField].alertPath, alert); + if (field && !manageDuplicate[alertFieldMapping[alertField].sirFieldKey].has(field)) { + manageDuplicate[alertFieldMapping[alertField].sirFieldKey].add(field); + acc = { + ...acc, + [alertFieldMapping[alertField].sirFieldKey]: `${ + acc[alertFieldMapping[alertField].sirFieldKey] != null + ? `${acc[alertFieldMapping[alertField].sirFieldKey]},${field}` + : field + }`, + }; + } + }); + return acc; + }, sirFields); } return { diff --git a/x-pack/plugins/cases/server/plugin.ts b/x-pack/plugins/cases/server/plugin.ts index 49220fc716034..bb1be163585a8 100644 --- a/x-pack/plugins/cases/server/plugin.ts +++ b/x-pack/plugins/cases/server/plugin.ts @@ -32,7 +32,6 @@ import type { CasesRequestHandlerContext } from './types'; import { CasesClientFactory } from './client/factory'; import { SpacesPluginStart } from '../../spaces/server'; import { PluginStartContract as FeaturesPluginStart } from '../../features/server'; -import { RuleRegistryPluginStartContract } from '../../rule_registry/server'; import { LensServerPluginSetup } from '../../lens/server'; function createConfig(context: PluginInitializerContext) { @@ -50,7 +49,6 @@ export interface PluginsStart { features: FeaturesPluginStart; spaces?: SpacesPluginStart; actions: ActionsPluginStart; - ruleRegistry?: RuleRegistryPluginStartContract; } /** @@ -139,13 +137,15 @@ export class CasePlugin { }, featuresPluginStart: plugins.features, actionsPluginStart: plugins.actions, - ruleRegistryPluginStart: plugins.ruleRegistry, lensEmbeddableFactory: this.lensEmbeddableFactory!, }); + const client = core.elasticsearch.client; + const getCasesClientWithRequest = async (request: KibanaRequest): Promise => { return this.clientFactory.create({ request, + scopedClusterClient: client.asScoped(request).asCurrentUser, savedObjectsService: core.savedObjects, }); }; @@ -171,6 +171,7 @@ export class CasePlugin { return this.clientFactory.create({ request, + scopedClusterClient: context.core.elasticsearch.client.asCurrentUser, savedObjectsService: savedObjects, }); }, diff --git a/x-pack/plugins/cases/server/services/alerts/index.test.ts b/x-pack/plugins/cases/server/services/alerts/index.test.ts index 0e1ad03a32af2..d7dd44b33628b 100644 --- a/x-pack/plugins/cases/server/services/alerts/index.test.ts +++ b/x-pack/plugins/cases/server/services/alerts/index.test.ts @@ -7,73 +7,280 @@ import { CaseStatuses } from '../../../common'; import { AlertService, AlertServiceContract } from '.'; -import { loggingSystemMock } from 'src/core/server/mocks'; -import { ruleRegistryMocks } from '../../../../rule_registry/server/mocks'; -import { AlertsClient } from '../../../../rule_registry/server'; -import { PublicMethodsOf } from '@kbn/utility-types'; +import { elasticsearchServiceMock, loggingSystemMock } from 'src/core/server/mocks'; +import { ALERT_WORKFLOW_STATUS } from '../../../../rule_registry/common/technical_rule_data_field_names'; describe('updateAlertsStatus', () => { + const esClient = elasticsearchServiceMock.createElasticsearchClient(); const logger = loggingSystemMock.create().get('case'); - let alertsClient: jest.Mocked>; - let alertService: AlertServiceContract; - - beforeEach(async () => { - alertsClient = ruleRegistryMocks.createAlertsClientMock.create(); - alertService = new AlertService(alertsClient); - jest.restoreAllMocks(); - }); describe('happy path', () => { - const args = { - alerts: [{ id: 'alert-id-1', index: '.siem-signals', status: CaseStatuses.closed }], - logger, - }; + let alertService: AlertServiceContract; + + beforeEach(async () => { + alertService = new AlertService(); + jest.resetAllMocks(); + }); it('updates the status of the alert correctly', async () => { + const args = { + alerts: [{ id: 'alert-id-1', index: '.siem-signals', status: CaseStatuses.closed }], + scopedClusterClient: esClient, + logger, + }; + await alertService.updateAlertsStatus(args); - expect(alertsClient.update).toHaveBeenCalledWith({ - id: 'alert-id-1', + expect(esClient.updateByQuery).toHaveBeenCalledWith({ index: '.siem-signals', - status: CaseStatuses.closed, + conflicts: 'abort', + body: { + script: { + source: `if (ctx._source['${ALERT_WORKFLOW_STATUS}'] != null) { + ctx._source['${ALERT_WORKFLOW_STATUS}'] = 'closed' + } + if (ctx._source.signal != null && ctx._source.signal.status != null) { + ctx._source.signal.status = 'closed' + }`, + lang: 'painless', + }, + query: { + ids: { + values: ['alert-id-1'], + }, + }, + }, + ignore_unavailable: true, }); }); - it('translates the in-progress status to acknowledged', async () => { - await alertService.updateAlertsStatus({ - alerts: [{ id: 'alert-id-1', index: '.siem-signals', status: CaseStatuses['in-progress'] }], + it('buckets the alerts by index', async () => { + const args = { + alerts: [ + { id: 'id1', index: '1', status: CaseStatuses.closed }, + { id: 'id2', index: '1', status: CaseStatuses.closed }, + ], + scopedClusterClient: esClient, logger, - }); + }; - expect(alertsClient.update).toHaveBeenCalledWith({ - id: 'alert-id-1', - index: '.siem-signals', - status: 'acknowledged', + await alertService.updateAlertsStatus(args); + + expect(esClient.updateByQuery).toBeCalledTimes(1); + expect(esClient.updateByQuery).toHaveBeenCalledWith({ + index: '1', + conflicts: 'abort', + body: { + script: { + source: `if (ctx._source['${ALERT_WORKFLOW_STATUS}'] != null) { + ctx._source['${ALERT_WORKFLOW_STATUS}'] = 'closed' + } + if (ctx._source.signal != null && ctx._source.signal.status != null) { + ctx._source.signal.status = 'closed' + }`, + lang: 'painless', + }, + query: { + ids: { + values: ['id1', 'id2'], + }, + }, + }, + ignore_unavailable: true, }); }); - it('defaults an unknown status to open', async () => { - await alertService.updateAlertsStatus({ - alerts: [{ id: 'alert-id-1', index: '.siem-signals', status: 'bananas' as CaseStatuses }], + it('translates in-progress to acknowledged', async () => { + const args = { + alerts: [{ id: 'id1', index: '1', status: CaseStatuses['in-progress'] }], + scopedClusterClient: esClient, logger, - }); + }; - expect(alertsClient.update).toHaveBeenCalledWith({ - id: 'alert-id-1', - index: '.siem-signals', - status: 'open', - }); + await alertService.updateAlertsStatus(args); + + expect(esClient.updateByQuery).toBeCalledTimes(1); + expect(esClient.updateByQuery.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + Object { + "body": Object { + "query": Object { + "ids": Object { + "values": Array [ + "id1", + ], + }, + }, + "script": Object { + "lang": "painless", + "source": "if (ctx._source['kibana.alert.workflow_status'] != null) { + ctx._source['kibana.alert.workflow_status'] = 'acknowledged' + } + if (ctx._source.signal != null && ctx._source.signal.status != null) { + ctx._source.signal.status = 'acknowledged' + }", + }, + }, + "conflicts": "abort", + "ignore_unavailable": true, + "index": "1", + }, + ] + `); + }); + + it('makes two calls when the statuses are different', async () => { + const args = { + alerts: [ + { id: 'id1', index: '1', status: CaseStatuses.closed }, + { id: 'id2', index: '1', status: CaseStatuses.open }, + ], + scopedClusterClient: esClient, + logger, + }; + + await alertService.updateAlertsStatus(args); + + expect(esClient.updateByQuery).toBeCalledTimes(2); + // id1 should be closed + expect(esClient.updateByQuery.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + Object { + "body": Object { + "query": Object { + "ids": Object { + "values": Array [ + "id1", + ], + }, + }, + "script": Object { + "lang": "painless", + "source": "if (ctx._source['kibana.alert.workflow_status'] != null) { + ctx._source['kibana.alert.workflow_status'] = 'closed' + } + if (ctx._source.signal != null && ctx._source.signal.status != null) { + ctx._source.signal.status = 'closed' + }", + }, + }, + "conflicts": "abort", + "ignore_unavailable": true, + "index": "1", + }, + ] + `); + + // id2 should be open + expect(esClient.updateByQuery.mock.calls[1]).toMatchInlineSnapshot(` + Array [ + Object { + "body": Object { + "query": Object { + "ids": Object { + "values": Array [ + "id2", + ], + }, + }, + "script": Object { + "lang": "painless", + "source": "if (ctx._source['kibana.alert.workflow_status'] != null) { + ctx._source['kibana.alert.workflow_status'] = 'open' + } + if (ctx._source.signal != null && ctx._source.signal.status != null) { + ctx._source.signal.status = 'open' + }", + }, + }, + "conflicts": "abort", + "ignore_unavailable": true, + "index": "1", + }, + ] + `); + }); + + it('makes two calls when the indices are different', async () => { + const args = { + alerts: [ + { id: 'id1', index: '1', status: CaseStatuses.closed }, + { id: 'id2', index: '2', status: CaseStatuses.open }, + ], + scopedClusterClient: esClient, + logger, + }; + + await alertService.updateAlertsStatus(args); + + expect(esClient.updateByQuery).toBeCalledTimes(2); + // id1 should be closed in index 1 + expect(esClient.updateByQuery.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + Object { + "body": Object { + "query": Object { + "ids": Object { + "values": Array [ + "id1", + ], + }, + }, + "script": Object { + "lang": "painless", + "source": "if (ctx._source['kibana.alert.workflow_status'] != null) { + ctx._source['kibana.alert.workflow_status'] = 'closed' + } + if (ctx._source.signal != null && ctx._source.signal.status != null) { + ctx._source.signal.status = 'closed' + }", + }, + }, + "conflicts": "abort", + "ignore_unavailable": true, + "index": "1", + }, + ] + `); + + // id2 should be open in index 2 + expect(esClient.updateByQuery.mock.calls[1]).toMatchInlineSnapshot(` + Array [ + Object { + "body": Object { + "query": Object { + "ids": Object { + "values": Array [ + "id2", + ], + }, + }, + "script": Object { + "lang": "painless", + "source": "if (ctx._source['kibana.alert.workflow_status'] != null) { + ctx._source['kibana.alert.workflow_status'] = 'open' + } + if (ctx._source.signal != null && ctx._source.signal.status != null) { + ctx._source.signal.status = 'open' + }", + }, + }, + "conflicts": "abort", + "ignore_unavailable": true, + "index": "2", + }, + ] + `); }); - }); - describe('unhappy path', () => { it('ignores empty indices', async () => { - expect( - await alertService.updateAlertsStatus({ - alerts: [{ id: 'alert-id-1', index: '', status: CaseStatuses.closed }], - logger, - }) - ).toBeUndefined(); + await alertService.updateAlertsStatus({ + alerts: [{ id: 'alert-id-1', index: '', status: CaseStatuses.open }], + scopedClusterClient: esClient, + logger, + }); + + expect(esClient.updateByQuery).not.toHaveBeenCalled(); }); }); }); diff --git a/x-pack/plugins/cases/server/services/alerts/index.ts b/x-pack/plugins/cases/server/services/alerts/index.ts index ccb0fca4f995f..6bb2fb3ee3c56 100644 --- a/x-pack/plugins/cases/server/services/alerts/index.ts +++ b/x-pack/plugins/cases/server/services/alerts/index.ts @@ -5,71 +5,62 @@ * 2.0. */ +import pMap from 'p-map'; import { isEmpty } from 'lodash'; import type { PublicMethodsOf } from '@kbn/utility-types'; -import { Logger } from 'kibana/server'; -import { CaseStatuses, MAX_ALERTS_PER_SUB_CASE } from '../../../common'; +import { ElasticsearchClient, Logger } from 'kibana/server'; +import { CaseStatuses, MAX_ALERTS_PER_SUB_CASE, MAX_CONCURRENT_SEARCHES } from '../../../common'; import { AlertInfo, createCaseError } from '../../common'; import { UpdateAlertRequest } from '../../client/alerts/types'; -import { AlertsClient } from '../../../../rule_registry/server'; -import { Alert } from './types'; -import { STATUS_VALUES } from '../../../../rule_registry/common/technical_rule_data_field_names'; +import { + ALERT_WORKFLOW_STATUS, + STATUS_VALUES, +} from '../../../../rule_registry/common/technical_rule_data_field_names'; export type AlertServiceContract = PublicMethodsOf; interface UpdateAlertsStatusArgs { alerts: UpdateAlertRequest[]; + scopedClusterClient: ElasticsearchClient; logger: Logger; } interface GetAlertsArgs { alertsInfo: AlertInfo[]; + scopedClusterClient: ElasticsearchClient; logger: Logger; } +interface Alert { + _id: string; + _index: string; + _source: Record; +} + +interface AlertsResponse { + docs: Alert[]; +} + function isEmptyAlert(alert: AlertInfo): boolean { return isEmpty(alert.id) || isEmpty(alert.index); } export class AlertService { - constructor(private readonly alertsClient?: PublicMethodsOf) {} + constructor() {} - public async updateAlertsStatus({ alerts, logger }: UpdateAlertsStatusArgs) { + public async updateAlertsStatus({ alerts, scopedClusterClient, logger }: UpdateAlertsStatusArgs) { try { - if (!this.alertsClient) { - throw new Error( - 'Alert client is undefined, the rule registry plugin must be enabled to updated the status of alerts' - ); - } - - const alertsToUpdate = alerts.filter((alert) => !isEmptyAlert(alert)); - - if (alertsToUpdate.length <= 0) { - return; - } - - const updatedAlerts = await Promise.allSettled( - alertsToUpdate.map((alert) => - this.alertsClient?.update({ - id: alert.id, - index: alert.index, - status: translateStatus({ alert, logger }), - _version: undefined, - }) - ) + const bucketedAlerts = bucketAlertsByIndexAndStatus(alerts, logger); + const indexBuckets = Array.from(bucketedAlerts.entries()); + + await pMap( + indexBuckets, + async (indexBucket: [string, Map]) => + updateByQuery(indexBucket, scopedClusterClient), + { concurrency: MAX_CONCURRENT_SEARCHES } ); - - updatedAlerts.forEach((updatedAlert, index) => { - if (updatedAlert.status === 'rejected') { - logger.error( - `Failed to update status for alert: ${JSON.stringify(alertsToUpdate[index])}: ${ - updatedAlert.reason - }` - ); - } - }); } catch (error) { throw createCaseError({ message: `Failed to update alert status ids: ${JSON.stringify(alerts)}: ${error}`, @@ -79,51 +70,25 @@ export class AlertService { } } - public async getAlerts({ alertsInfo, logger }: GetAlertsArgs): Promise { + public async getAlerts({ + scopedClusterClient, + alertsInfo, + logger, + }: GetAlertsArgs): Promise { try { - if (!this.alertsClient) { - throw new Error( - 'Alert client is undefined, the rule registry plugin must be enabled to retrieve alerts' - ); - } + const docs = alertsInfo + .filter((alert) => !isEmptyAlert(alert)) + .slice(0, MAX_ALERTS_PER_SUB_CASE) + .map((alert) => ({ _id: alert.id, _index: alert.index })); - const alertsToGet = alertsInfo - .filter((alert) => !isEmpty(alert)) - .slice(0, MAX_ALERTS_PER_SUB_CASE); - - if (alertsToGet.length <= 0) { + if (docs.length <= 0) { return; } - const retrievedAlerts = await Promise.allSettled( - alertsToGet.map(({ id, index }) => this.alertsClient?.get({ id, index })) - ); - - retrievedAlerts.forEach((alert, index) => { - if (alert.status === 'rejected') { - logger.error( - `Failed to retrieve alert: ${JSON.stringify(alertsToGet[index])}: ${alert.reason}` - ); - } - }); + const results = await scopedClusterClient.mget({ body: { docs } }); - return retrievedAlerts.map((alert, index) => { - let source: unknown | undefined; - let error: Error | undefined; - - if (alert.status === 'fulfilled') { - source = alert.value; - } else { - error = alert.reason; - } - - return { - id: alertsToGet[index].id, - index: alertsToGet[index].index, - source, - error, - }; - }); + // @ts-expect-error @elastic/elasticsearch _source is optional + return results.body; } catch (error) { throw createCaseError({ message: `Failed to retrieve alerts ids: ${JSON.stringify(alertsInfo)}: ${error}`, @@ -134,6 +99,44 @@ export class AlertService { } } +interface TranslatedUpdateAlertRequest { + id: string; + index: string; + status: STATUS_VALUES; +} + +function bucketAlertsByIndexAndStatus( + alerts: UpdateAlertRequest[], + logger: Logger +): Map> { + return alerts.reduce>>( + (acc, alert) => { + // skip any alerts that are empty + if (isEmptyAlert(alert)) { + return acc; + } + + const translatedAlert = { ...alert, status: translateStatus({ alert, logger }) }; + const statusToAlertId = acc.get(translatedAlert.index); + + // if we haven't seen the index before + if (!statusToAlertId) { + // add a new index in the parent map, with an entry for the status the alert set to pointing + // to an initial array of only the current alert + acc.set(translatedAlert.index, createStatusToAlertMap(translatedAlert)); + } else { + // We had the index in the map so check to see if we have a bucket for the + // status, if not add a new status entry with the alert, if so update the status entry + // with the alert + updateIndexEntryWithStatus(statusToAlertId, translatedAlert); + } + + return acc; + }, + new Map() + ); +} + function translateStatus({ alert, logger, @@ -157,3 +160,53 @@ function translateStatus({ } return translatedStatus ?? 'open'; } + +function createStatusToAlertMap( + alert: TranslatedUpdateAlertRequest +): Map { + return new Map([[alert.status, [alert]]]); +} + +function updateIndexEntryWithStatus( + statusToAlerts: Map, + alert: TranslatedUpdateAlertRequest +) { + const statusBucket = statusToAlerts.get(alert.status); + + if (!statusBucket) { + statusToAlerts.set(alert.status, [alert]); + } else { + statusBucket.push(alert); + } +} + +async function updateByQuery( + [index, statusToAlertMap]: [string, Map], + scopedClusterClient: ElasticsearchClient +) { + const statusBuckets = Array.from(statusToAlertMap); + return Promise.all( + // this will create three update by query calls one for each of the three statuses + statusBuckets.map(([status, translatedAlerts]) => + scopedClusterClient.updateByQuery({ + index, + conflicts: 'abort', + body: { + script: { + source: `if (ctx._source['${ALERT_WORKFLOW_STATUS}'] != null) { + ctx._source['${ALERT_WORKFLOW_STATUS}'] = '${status}' + } + if (ctx._source.signal != null && ctx._source.signal.status != null) { + ctx._source.signal.status = '${status}' + }`, + lang: 'painless', + }, + // the query here will contain all the ids that have the same status for the same index + // being updated + query: { ids: { values: translatedAlerts.map(({ id }) => id) } }, + }, + ignore_unavailable: true, + }) + ) + ); +} diff --git a/x-pack/plugins/cases/server/services/alerts/types.ts b/x-pack/plugins/cases/server/services/alerts/types.ts deleted file mode 100644 index 5ddc57fa5861c..0000000000000 --- a/x-pack/plugins/cases/server/services/alerts/types.ts +++ /dev/null @@ -1,13 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -export interface Alert { - id: string; - index: string; - error?: Error; - source?: unknown; -} diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_alerts/alerts_details.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_alerts/alerts_details.spec.ts index f5cbc65effd85..674114188632b 100644 --- a/x-pack/plugins/security_solution/cypress/integration/detection_alerts/alerts_details.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/detection_alerts/alerts_details.spec.ts @@ -54,7 +54,7 @@ describe('Alert details with unmapped fields', () => { it('Displays the unmapped field on the table', () => { const expectedUnmmappedField = { - row: 90, + row: 86, field: 'unmapped', text: 'This is the unmapped field', }; diff --git a/x-pack/plugins/security_solution/cypress/tasks/alerts.ts b/x-pack/plugins/security_solution/cypress/tasks/alerts.ts index 1520a88ec31bc..871ef0ca51ce3 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/alerts.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/alerts.ts @@ -63,7 +63,12 @@ export const closeAlerts = () => { }; export const expandFirstAlert = () => { - cy.get(EXPAND_ALERT_BTN).should('exist').first().click({ force: true }); + cy.get(EXPAND_ALERT_BTN).should('exist'); + + cy.get(EXPAND_ALERT_BTN) + .first() + .pipe(($el) => $el.trigger('click')) + .should('exist'); }; export const viewThreatIntelTab = () => cy.get(THREAT_INTEL_TAB).click(); diff --git a/x-pack/plugins/security_solution/cypress/tasks/create_new_rule.ts b/x-pack/plugins/security_solution/cypress/tasks/create_new_rule.ts index f34c3f598e934..e2d27a11ed717 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/create_new_rule.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/create_new_rule.ts @@ -532,7 +532,6 @@ export const waitForAlertsToPopulate = async (alertCountThreshold = 1) => { cy.waitUntil( () => { refreshPage(); - cy.get(LOADING_INDICATOR).should('exist'); cy.get(LOADING_INDICATOR).should('not.exist'); return cy .get(SERVER_SIDE_EVENT_COUNT) diff --git a/x-pack/plugins/security_solution/public/app/deep_links/index.test.ts b/x-pack/plugins/security_solution/public/app/deep_links/index.test.ts index 4df49b957ad9c..59af6737e495f 100644 --- a/x-pack/plugins/security_solution/public/app/deep_links/index.test.ts +++ b/x-pack/plugins/security_solution/public/app/deep_links/index.test.ts @@ -101,68 +101,4 @@ describe('public search functions', () => { }); expect(deepLinks.some((l) => l.id === SecurityPageName.ueba)).toBeTruthy(); }); - - describe('Detections Alerts deep links', () => { - it('should return alerts link for basic license with only read_alerts capabilities', () => { - const basicLicense = 'basic'; - const basicLinks = getDeepLinks(mockGlobalState.app.enableExperimental, basicLicense, ({ - siem: { read_alerts: true, crud_alerts: false }, - } as unknown) as Capabilities); - - const detectionsDeepLinks = - basicLinks.find((l) => l.id === SecurityPageName.detections)?.deepLinks ?? []; - - expect( - detectionsDeepLinks.length && - detectionsDeepLinks.some((l) => l.id === SecurityPageName.alerts) - ).toBeTruthy(); - }); - - it('should return alerts link with for basic license with crud_alerts capabilities', () => { - const basicLicense = 'basic'; - const basicLinks = getDeepLinks(mockGlobalState.app.enableExperimental, basicLicense, ({ - siem: { read_alerts: true, crud_alerts: true }, - } as unknown) as Capabilities); - - const detectionsDeepLinks = - basicLinks.find((l) => l.id === SecurityPageName.detections)?.deepLinks ?? []; - - expect( - detectionsDeepLinks.length && - detectionsDeepLinks.some((l) => l.id === SecurityPageName.alerts) - ).toBeTruthy(); - }); - - it('should NOT return alerts link for basic license with NO read_alerts capabilities', () => { - const basicLicense = 'basic'; - const basicLinks = getDeepLinks(mockGlobalState.app.enableExperimental, basicLicense, ({ - siem: { read_alerts: false, crud_alerts: false }, - } as unknown) as Capabilities); - - const detectionsDeepLinks = - basicLinks.find((l) => l.id === SecurityPageName.detections)?.deepLinks ?? []; - - expect( - detectionsDeepLinks.length && - detectionsDeepLinks.some((l) => l.id === SecurityPageName.alerts) - ).toBeFalsy(); - }); - - it('should return alerts link for basic license with undefined capabilities', () => { - const basicLicense = 'basic'; - const basicLinks = getDeepLinks( - mockGlobalState.app.enableExperimental, - basicLicense, - undefined - ); - - const detectionsDeepLinks = - basicLinks.find((l) => l.id === SecurityPageName.detections)?.deepLinks ?? []; - - expect( - detectionsDeepLinks.length && - detectionsDeepLinks.some((l) => l.id === SecurityPageName.alerts) - ).toBeTruthy(); - }); - }); }); diff --git a/x-pack/plugins/security_solution/public/app/deep_links/index.ts b/x-pack/plugins/security_solution/public/app/deep_links/index.ts index bafab2dd659f4..9f13a8be0e13a 100644 --- a/x-pack/plugins/security_solution/public/app/deep_links/index.ts +++ b/x-pack/plugins/security_solution/public/app/deep_links/index.ts @@ -368,16 +368,7 @@ export function getDeepLinks( deepLinks: [], }; } - if ( - deepLinkId === SecurityPageName.detections && - capabilities != null && - capabilities.siem.read_alerts === false - ) { - return { - ...deepLink, - deepLinks: baseDeepLinks.filter(({ id }) => id !== SecurityPageName.alerts), - }; - } + if (isPremiumLicense(licenseType) && subPluginDeepLinks?.premium) { return { ...deepLink, diff --git a/x-pack/plugins/security_solution/public/cases/components/case_view/index.tsx b/x-pack/plugins/security_solution/public/cases/components/case_view/index.tsx index 3ec616127f243..7041cc4264504 100644 --- a/x-pack/plugins/security_solution/public/cases/components/case_view/index.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/case_view/index.tsx @@ -59,7 +59,7 @@ const TimelineDetailsPanel = () => { diff --git a/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.test.tsx b/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.test.tsx index 29ba8fc0bd541..7b7a1ead5d702 100644 --- a/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.test.tsx @@ -33,14 +33,6 @@ import { useIsExperimentalFeatureEnabled } from '../../hooks/use_experimental_fe import { defaultCellActions } from '../../lib/cell_actions/default_cell_actions'; import { mockTimelines } from '../../mock/mock_timelines_plugin'; -jest.mock('@kbn/alerts', () => ({ - useGetUserAlertsPermissions: () => ({ - loading: false, - crud: true, - read: true, - }), -})); - jest.mock('../../lib/kibana', () => ({ useKibana: () => ({ services: { diff --git a/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx b/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx index 70fd80a13555b..e423776251bfc 100644 --- a/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx @@ -5,8 +5,8 @@ * 2.0. */ -import React, { useMemo, useEffect } from 'react'; -import { connect, ConnectedProps } from 'react-redux'; +import React, { useCallback, useMemo, useEffect } from 'react'; +import { connect, ConnectedProps, useDispatch } from 'react-redux'; import deepEqual from 'fast-deep-equal'; import styled from 'styled-components'; @@ -108,6 +108,7 @@ const StatefulEventsViewerComponent: React.FC = ({ hasAlertsCrud = false, unit, }) => { + const dispatch = useDispatch(); const { timelines: timelinesUi } = useKibana().services; const { browserFields, @@ -149,6 +150,13 @@ const StatefulEventsViewerComponent: React.FC = ({ ) : null, [graphEventId, id] ); + const setQuery = useCallback( + (inspect, loading, refetch) => { + dispatch(inputsActions.setQuery({ id, inputId: 'global', inspect, loading, refetch })); + }, + [dispatch, id] + ); + return ( <> @@ -180,6 +188,7 @@ const StatefulEventsViewerComponent: React.FC = ({ onRuleChange, renderCellValue, rowRenderers, + setQuery, start, sort, additionalFilters, diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/index.test.tsx index 1f98d3b826129..b488000ac8736 100644 --- a/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/index.test.tsx @@ -4,7 +4,6 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { useGetUserAlertsPermissions } from '@kbn/alerts'; import { renderHook } from '@testing-library/react-hooks'; import { KibanaPageTemplateProps } from '../../../../../../../../src/plugins/kibana_react/public'; @@ -24,7 +23,6 @@ jest.mock('../../../lib/kibana'); jest.mock('../../../hooks/use_selector'); jest.mock('../../../hooks/use_experimental_features'); jest.mock('../../../utils/route/use_route_spy'); -jest.mock('@kbn/alerts'); describe('useSecuritySolutionNavigation', () => { const mockUrlState = { [CONSTANTS.appQuery]: { query: 'host.name:"security-solution-es"', language: 'kuery' }, @@ -76,11 +74,6 @@ describe('useSecuritySolutionNavigation', () => { (useIsExperimentalFeatureEnabled as jest.Mock).mockReturnValue(false); (useDeepEqualSelector as jest.Mock).mockReturnValue({ urlState: mockUrlState }); (useRouteSpy as jest.Mock).mockReturnValue(mockRouteSpy); - (useGetUserAlertsPermissions as jest.Mock).mockReturnValue({ - loading: false, - crud: true, - read: true, - }); (useKibana as jest.Mock).mockReturnValue({ services: { diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/use_navigation_items.tsx b/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/use_navigation_items.tsx index ca574a5872761..1630bc47fd0c3 100644 --- a/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/use_navigation_items.tsx +++ b/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/use_navigation_items.tsx @@ -7,15 +7,13 @@ import React, { useCallback, useMemo } from 'react'; import { EuiSideNavItemType } from '@elastic/eui/src/components/side_nav/side_nav_types'; -import { useGetUserAlertsPermissions } from '@kbn/alerts'; import { securityNavGroup } from '../../../../app/home/home_navigations'; import { getSearch } from '../helpers'; import { PrimaryNavigationItemsProps } from './types'; -import { useGetUserCasesPermissions, useKibana } from '../../../lib/kibana'; +import { useGetUserCasesPermissions } from '../../../lib/kibana'; import { useNavigation } from '../../../lib/kibana/hooks'; import { NavTab } from '../types'; -import { SERVER_APP_ID } from '../../../../../common/constants'; export const usePrimaryNavigationItems = ({ navTabs, @@ -63,9 +61,7 @@ export const usePrimaryNavigationItems = ({ }; function usePrimaryNavigationItemsToDisplay(navTabs: Record) { - const uiCapabilities = useKibana().services.application.capabilities; const hasCasesReadPermissions = useGetUserCasesPermissions()?.read; - const hasAlertsReadPermissions = useGetUserAlertsPermissions(uiCapabilities, SERVER_APP_ID); return useMemo( () => [ { @@ -75,9 +71,7 @@ function usePrimaryNavigationItemsToDisplay(navTabs: Record) { }, { ...securityNavGroup.detect, - items: hasAlertsReadPermissions.read - ? [navTabs.alerts, navTabs.rules, navTabs.exceptions] - : [navTabs.rules, navTabs.exceptions], + items: [navTabs.alerts, navTabs.rules, navTabs.exceptions], }, { ...securityNavGroup.explore, @@ -92,6 +86,6 @@ function usePrimaryNavigationItemsToDisplay(navTabs: Record) { items: [navTabs.endpoints, navTabs.trusted_apps, navTabs.event_filters], }, ], - [navTabs, hasCasesReadPermissions, hasAlertsReadPermissions] + [navTabs, hasCasesReadPermissions] ); } diff --git a/x-pack/plugins/security_solution/public/common/components/user_privileges/index.tsx b/x-pack/plugins/security_solution/public/common/components/user_privileges/index.tsx index fa9de895f7d03..028473f5c2001 100644 --- a/x-pack/plugins/security_solution/public/common/components/user_privileges/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/user_privileges/index.tsx @@ -5,9 +5,8 @@ * 2.0. */ -import React, { createContext, useContext } from 'react'; +import React, { createContext, useContext, useEffect, useState } from 'react'; import { DeepReadonly } from 'utility-types'; -import { useGetUserAlertsPermissions } from '@kbn/alerts'; import { Capabilities } from '../../../../../../../src/core/public'; import { useFetchDetectionEnginePrivileges } from '../../../detections/components/user_privileges/use_fetch_detection_engine_privileges'; @@ -19,14 +18,14 @@ export interface UserPrivilegesState { listPrivileges: ReturnType; detectionEnginePrivileges: ReturnType; endpointPrivileges: EndpointPrivileges; - alertsPrivileges: ReturnType; + kibanaSecuritySolutionsPrivileges: { crud: boolean; read: boolean }; } export const initialUserPrivilegesState = (): UserPrivilegesState => ({ listPrivileges: { loading: false, error: undefined, result: undefined }, detectionEnginePrivileges: { loading: false, error: undefined, result: undefined }, endpointPrivileges: { loading: true, canAccessEndpointManagement: false, canAccessFleet: false }, - alertsPrivileges: { loading: false, read: false, crud: false }, + kibanaSecuritySolutionsPrivileges: { crud: false, read: false }, }); const UserPrivilegesContext = createContext(initialUserPrivilegesState()); @@ -43,14 +42,29 @@ export const UserPrivilegesProvider = ({ const listPrivileges = useFetchListPrivileges(); const detectionEnginePrivileges = useFetchDetectionEnginePrivileges(); const endpointPrivileges = useEndpointPrivileges(); - const alertsPrivileges = useGetUserAlertsPermissions(kibanaCapabilities, SERVER_APP_ID); + const [kibanaSecuritySolutionsPrivileges, setKibanaSecuritySolutionsPrivileges] = useState({ + crud: false, + read: false, + }); + const crud: boolean = kibanaCapabilities[SERVER_APP_ID].crud === true; + const read: boolean = kibanaCapabilities[SERVER_APP_ID].show === true; + + useEffect(() => { + setKibanaSecuritySolutionsPrivileges((currPrivileges) => { + if (currPrivileges.read !== read || currPrivileges.crud !== crud) { + return { read, crud }; + } + return currPrivileges; + }); + }, [crud, read]); + return ( {children} diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_count_panel/index.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_count_panel/index.tsx index 9cc844a80b031..6bd902658c8e4 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_count_panel/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_count_panel/index.tsx @@ -14,8 +14,6 @@ import { HeaderSection } from '../../../../common/components/header_section'; import { useQueryAlerts } from '../../../containers/detection_engine/alerts/use_query'; import { InspectButtonContainer } from '../../../../common/components/inspect'; -import { fetchQueryRuleRegistryAlerts } from '../../../containers/detection_engine/alerts/api'; - import { getAlertsCountQuery } from './helpers'; import * as i18n from './translations'; import { AlertsCount } from './alerts_count'; @@ -49,7 +47,8 @@ export const AlertsCountPanel = memo( // ? fetchQueryRuleRegistryAlerts // : fetchQueryAlerts; - const fetchMethod = fetchQueryRuleRegistryAlerts; + // Disabling the fecth method in useQueryAlerts since it is defaulted to the old one + // const fetchMethod = fetchQueryRuleRegistryAlerts; const additionalFilters = useMemo(() => { try { @@ -73,7 +72,6 @@ export const AlertsCountPanel = memo( request, refetch, } = useQueryAlerts<{}, AlertsCountAggregation>({ - fetchMethod, query: getAlertsCountQuery(selectedStackByOption, from, to, additionalFilters), indexName: signalIndexName, }); diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_histogram_panel/index.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_histogram_panel/index.tsx index b296371bae58d..2182ed7da0c4f 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_histogram_panel/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_histogram_panel/index.tsx @@ -43,7 +43,6 @@ import type { AlertsStackByField } from '../common/types'; import { KpiPanel, StackBySelect } from '../common/components'; import { useInspectButton } from '../common/hooks'; -import { fetchQueryRuleRegistryAlerts } from '../../../containers/detection_engine/alerts/api'; const defaultTotalAlertsObj: AlertsTotal = { value: 0, @@ -117,16 +116,12 @@ export const AlertsHistogramPanel = memo( request, refetch, } = useQueryAlerts<{}, AlertsAggregation>({ - fetchMethod: fetchQueryRuleRegistryAlerts, - query: { - index: signalIndexName, - ...getAlertsHistogramQuery( - selectedStackByOption, - from, - to, - buildCombinedQueries(combinedQueries) - ), - }, + query: getAlertsHistogramQuery( + selectedStackByOption, + from, + to, + buildCombinedQueries(combinedQueries) + ), indexName: signalIndexName, }); diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx index 4b3c792319cd1..e179c02987462 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx @@ -381,7 +381,7 @@ export const AlertsTableComponent: React.FC = ({ pageFilters={defaultFiltersMemo} defaultCellActions={defaultCellActions} defaultModel={defaultTimelineModel} - entityType="alerts" + entityType="events" end={to} currentFilter={filterGroup} id={timelineId} @@ -392,7 +392,7 @@ export const AlertsTableComponent: React.FC = ({ start={from} utilityBar={utilityBarCallback} additionalFilters={additionalFiltersComponent} - hasAlertsCrud={hasIndexWrite} + hasAlertsCrud={hasIndexWrite && hasIndexMaintenance} /> ); }; diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.test.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.test.tsx index eb31a59f0ca87..9568f9c894e24 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.test.tsx @@ -13,14 +13,6 @@ import React from 'react'; import { Ecs } from '../../../../../common/ecs'; import { mockTimelines } from '../../../../common/mock/mock_timelines_plugin'; -jest.mock('@kbn/alerts', () => ({ - useGetUserAlertsPermissions: () => ({ - loading: false, - crud: true, - read: true, - }), -})); - const ecsRowData: Ecs = { _id: '1', agent: { type: ['blah'] } }; const props = { diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_alerts_actions.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_alerts_actions.tsx index 3568972aef2e9..8da4ce1c3ed7f 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_alerts_actions.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_alerts_actions.tsx @@ -7,15 +7,12 @@ import { useCallback } from 'react'; import { useDispatch } from 'react-redux'; -import { useGetUserAlertsPermissions } from '@kbn/alerts'; import { useStatusBulkActionItems } from '../../../../../../timelines/public'; import { Status } from '../../../../../common/detection_engine/schemas/common/schemas'; import { timelineActions } from '../../../../timelines/store/timeline'; +import { useAlertsPrivileges } from '../../../containers/detection_engine/alerts/use_alerts_privileges'; import { SetEventsDeletedProps, SetEventsLoadingProps } from '../types'; - -import { useKibana } from '../../../../common/lib/kibana'; -import { SERVER_APP_ID } from '../../../../../common/constants'; interface Props { alertStatus?: Status; closePopover: () => void; @@ -34,8 +31,7 @@ export const useAlertsActions = ({ refetch, }: Props) => { const dispatch = useDispatch(); - const uiCapabilities = useKibana().services.application.capabilities; - const alertsPrivileges = useGetUserAlertsPermissions(uiCapabilities, SERVER_APP_ID); + const { hasIndexWrite, hasKibanaCRUD } = useAlertsPrivileges(); const onStatusUpdate = useCallback(() => { closePopover(); @@ -66,9 +62,10 @@ export const useAlertsActions = ({ setEventsDeleted, onUpdateSuccess: onStatusUpdate, onUpdateFailure: onStatusUpdate, + timelineId, }); return { - actionItems: alertsPrivileges.crud ? actionItems : [], + actionItems: hasIndexWrite && hasKibanaCRUD ? actionItems : [], }; }; diff --git a/x-pack/plugins/security_solution/public/detections/components/callouts/missing_privileges_callout/translations.tsx b/x-pack/plugins/security_solution/public/detections/components/callouts/missing_privileges_callout/translations.tsx index 3509ad73001ec..0d628d89c0925 100644 --- a/x-pack/plugins/security_solution/public/detections/components/callouts/missing_privileges_callout/translations.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/callouts/missing_privileges_callout/translations.tsx @@ -9,10 +9,6 @@ import { EuiCode } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import React from 'react'; -import { - DetectionsRequirementsLink, - SecuritySolutionRequirementsLink, -} from '../../../../common/components/links_to_docs'; import { DEFAULT_ITEMS_INDEX, DEFAULT_LISTS_INDEX, @@ -21,6 +17,10 @@ import { } from '../../../../../common/constants'; import { CommaSeparatedValues } from './comma_separated_values'; import { MissingPrivileges } from './use_missing_privileges'; +import { + DetectionsRequirementsLink, + SecuritySolutionRequirementsLink, +} from '../../../../common/components/links_to_docs'; export const MISSING_PRIVILEGES_CALLOUT_TITLE = i18n.translate( 'xpack.securitySolution.detectionEngine.missingPrivilegesCallOut.messageTitle', @@ -46,17 +46,17 @@ const CANNOT_EDIT_LISTS = i18n.translate( const CANNOT_EDIT_ALERTS = i18n.translate( 'xpack.securitySolution.detectionEngine.missingPrivilegesCallOut.cannotEditAlerts', { - defaultMessage: 'Without these privileges, you cannot open or close alerts.', + defaultMessage: 'Without these privileges, you cannot view or change status of alerts.', } ); export const missingPrivilegesCallOutBody = ({ indexPrivileges, - featurePrivileges, + featurePrivileges = [], }: MissingPrivileges) => ( @@ -77,23 +77,30 @@ export const missingPrivilegesCallOutBody = ({ {indexPrivileges.map(([index, missingPrivileges]) => (
  • {missingIndexPrivileges(index, missingPrivileges)}
  • ))} - - - ) : null, - featurePrivileges: - featurePrivileges.length > 0 ? ( - <> - -
      - {featurePrivileges.map(([feature, missingPrivileges]) => ( + { + // TODO: Uncomment once RBAC for alerts is reenabled + /* {featurePrivileges.map(([feature, missingPrivileges]) => (
    • {missingFeaturePrivileges(feature, missingPrivileges)}
    • - ))} + ))} */ + }
    ) : null, + // TODO: Uncomment once RBAC for alerts is reenabled + // featurePrivileges: + // featurePrivileges.length > 0 ? ( + // <> + // + //
      + // {featurePrivileges.map(([feature, missingPrivileges]) => ( + //
    • {missingFeaturePrivileges(feature, missingPrivileges)}
    • + // ))} + //
    + // + // ) : null, docs: (

    My Canvas Workpad
    " +
    markdown mock
    My Canvas Workpad
    " `; exports[`Canvas Shareable Workpad API Placed successfully with height specified 1`] = `"
    "`; @@ -21,7 +21,7 @@ exports[`Canvas Shareable Workpad API Placed successfully with height specified
    markdown mock
    markdown mock
    My Canvas Workpad
    " +
    markdown mock
    My Canvas Workpad
    " `; exports[`Canvas Shareable Workpad API Placed successfully with page specified 1`] = `"
    "`; @@ -33,7 +33,7 @@ exports[`Canvas Shareable Workpad API Placed successfully with page specified 2`
    markdown mock
    markdown mock
    My Canvas Workpad
    " +
    markdown mock
    My Canvas Workpad
    " `; exports[`Canvas Shareable Workpad API Placed successfully with width and height specified 1`] = `"
    "`; @@ -45,7 +45,7 @@ exports[`Canvas Shareable Workpad API Placed successfully with width and height
    markdown mock
    markdown mock
    My Canvas Workpad
    " +
    markdown mock
    My Canvas Workpad
    " `; exports[`Canvas Shareable Workpad API Placed successfully with width specified 1`] = `"
    "`; @@ -57,5 +57,5 @@ exports[`Canvas Shareable Workpad API Placed successfully with width specified 2
    markdown mock
    markdown mock
    My Canvas Workpad
    " +
    markdown mock
    My Canvas Workpad
    " `; diff --git a/x-pack/plugins/canvas/shareable_runtime/components/__stories__/__snapshots__/canvas.stories.storyshot b/x-pack/plugins/canvas/shareable_runtime/components/__stories__/__snapshots__/canvas.stories.storyshot index c5b6d768c89d8..a5eefde192371 100644 --- a/x-pack/plugins/canvas/shareable_runtime/components/__stories__/__snapshots__/canvas.stories.storyshot +++ b/x-pack/plugins/canvas/shareable_runtime/components/__stories__/__snapshots__/canvas.stories.storyshot @@ -1375,7 +1375,7 @@ exports[`Storyshots shareables/Canvas component 1`] = ` >