diff --git a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddableeditorstate.md b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddableeditorstate.md index b944c9dcc02a2..07ae46f8bbf12 100644 --- a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddableeditorstate.md +++ b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddableeditorstate.md @@ -18,6 +18,7 @@ export interface EmbeddableEditorState | --- | --- | --- | | [embeddableId](./kibana-plugin-plugins-embeddable-public.embeddableeditorstate.embeddableid.md) | string | | | [originatingApp](./kibana-plugin-plugins-embeddable-public.embeddableeditorstate.originatingapp.md) | string | | +| [originatingPath](./kibana-plugin-plugins-embeddable-public.embeddableeditorstate.originatingpath.md) | string | | | [searchSessionId](./kibana-plugin-plugins-embeddable-public.embeddableeditorstate.searchsessionid.md) | string | Pass current search session id when navigating to an editor, Editors could use it continue previous search session | | [valueInput](./kibana-plugin-plugins-embeddable-public.embeddableeditorstate.valueinput.md) | EmbeddableInput | | diff --git a/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddableeditorstate.originatingpath.md b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddableeditorstate.originatingpath.md new file mode 100644 index 0000000000000..e255f11f8a059 --- /dev/null +++ b/docs/development/plugins/embeddable/public/kibana-plugin-plugins-embeddable-public.embeddableeditorstate.originatingpath.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-embeddable-public](./kibana-plugin-plugins-embeddable-public.md) > [EmbeddableEditorState](./kibana-plugin-plugins-embeddable-public.embeddableeditorstate.md) > [originatingPath](./kibana-plugin-plugins-embeddable-public.embeddableeditorstate.originatingpath.md) + +## EmbeddableEditorState.originatingPath property + +Signature: + +```typescript +originatingPath?: string; +``` diff --git a/package.json b/package.json index 00fa0807e0f93..205685cd5389c 100644 --- a/package.json +++ b/package.json @@ -95,7 +95,7 @@ "dependencies": { "@elastic/apm-rum": "^5.8.0", "@elastic/apm-rum-react": "^1.2.11", - "@elastic/charts": "33.2.2", + "@elastic/charts": "34.0.0", "@elastic/datemath": "link:bazel-bin/packages/elastic-datemath", "@elastic/elasticsearch": "npm:@elastic/elasticsearch-canary@^8.0.0-canary.17", "@elastic/ems-client": "7.15.0", @@ -379,6 +379,7 @@ "redux-saga": "^1.1.3", "redux-thunk": "^2.3.0", "redux-thunks": "^1.0.0", + "remark-stringify": "^9.0.0", "regenerator-runtime": "^0.13.3", "request": "^2.88.0", "require-in-the-middle": "^5.0.2", diff --git a/src/plugins/embeddable/public/lib/state_transfer/types.ts b/src/plugins/embeddable/public/lib/state_transfer/types.ts index 98cf6e70284cd..74ee31ba71104 100644 --- a/src/plugins/embeddable/public/lib/state_transfer/types.ts +++ b/src/plugins/embeddable/public/lib/state_transfer/types.ts @@ -17,6 +17,7 @@ export const EMBEDDABLE_EDITOR_STATE_KEY = 'embeddable_editor_state'; */ export interface EmbeddableEditorState { originatingApp: string; + originatingPath?: string; embeddableId?: string; valueInput?: EmbeddableInput; diff --git a/src/plugins/embeddable/public/public.api.md b/src/plugins/embeddable/public/public.api.md index 2e46cb82dc592..3dfe10445fb85 100644 --- a/src/plugins/embeddable/public/public.api.md +++ b/src/plugins/embeddable/public/public.api.md @@ -369,6 +369,8 @@ export interface EmbeddableEditorState { embeddableId?: string; // (undocumented) originatingApp: string; + // (undocumented) + originatingPath?: string; searchSessionId?: string; // (undocumented) valueInput?: EmbeddableInput; diff --git a/src/plugins/saved_objects/public/finder/index.ts b/src/plugins/saved_objects/public/finder/index.ts index edec012d90d6f..de6a54795fce5 100644 --- a/src/plugins/saved_objects/public/finder/index.ts +++ b/src/plugins/saved_objects/public/finder/index.ts @@ -9,5 +9,6 @@ export { SavedObjectMetaData, SavedObjectFinderUi, + SavedObjectFinderUiProps, getSavedObjectFinder, } from './saved_object_finder'; diff --git a/src/plugins/saved_objects/public/index.ts b/src/plugins/saved_objects/public/index.ts index 84c39168d82c2..bc84298a63717 100644 --- a/src/plugins/saved_objects/public/index.ts +++ b/src/plugins/saved_objects/public/index.ts @@ -17,7 +17,12 @@ export { SaveResult, showSaveModal, } from './save_modal'; -export { getSavedObjectFinder, SavedObjectFinderUi, SavedObjectMetaData } from './finder'; +export { + getSavedObjectFinder, + SavedObjectFinderUi, + SavedObjectFinderUiProps, + SavedObjectMetaData, +} from './finder'; export { SavedObjectLoader, SavedObjectLoaderFindOptions, diff --git a/x-pack/examples/embedded_lens_example/public/app.tsx b/x-pack/examples/embedded_lens_example/public/app.tsx index 58c932c3ca164..55f2b4ccd71e9 100644 --- a/x-pack/examples/embedded_lens_example/public/app.tsx +++ b/x-pack/examples/embedded_lens_example/public/app.tsx @@ -173,7 +173,9 @@ export const App = (props: { timeRange: time, attributes: getLensAttributes(props.defaultIndexPattern!, color), }, - true + { + openInNewTab: true, + } ); // eslint-disable-next-line no-bitwise const newColor = '#' + ((Math.random() * 0xffffff) << 0).toString(16); @@ -195,7 +197,9 @@ export const App = (props: { timeRange: time, attributes: getLensAttributes(props.defaultIndexPattern!, color), }, - false + { + openInNewTab: false, + } ); }} > diff --git a/x-pack/plugins/apm/common/agent_name.ts b/x-pack/plugins/apm/common/agent_name.ts index 650e72751749e..b41ae949d5867 100644 --- a/x-pack/plugins/apm/common/agent_name.ts +++ b/x-pack/plugins/apm/common/agent_name.ts @@ -86,3 +86,7 @@ export function isIosAgentName(agentName?: string) { const lowercased = agentName && agentName.toLowerCase(); return lowercased === 'ios/swift' || lowercased === 'opentelemetry/swift'; } + +export function isJRubyAgent(agentName?: string, runtimeName?: string) { + return agentName === 'ruby' && runtimeName?.toLowerCase() === 'jruby'; +} diff --git a/x-pack/plugins/apm/public/components/app/transaction_overview/transaction_overview.test.tsx b/x-pack/plugins/apm/public/components/app/transaction_overview/transaction_overview.test.tsx index c7dd0f46cfc22..8d7d14191a851 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_overview/transaction_overview.test.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_overview/transaction_overview.test.tsx @@ -16,7 +16,7 @@ import { UrlParamsProvider } from '../../../context/url_params_context/url_param import { IUrlParams } from '../../../context/url_params_context/types'; import * as useFetcherHook from '../../../hooks/use_fetcher'; import * as useServiceTransactionTypesHook from '../../../context/apm_service/use_service_transaction_types_fetcher'; -import * as useServiceAgentNameHook from '../../../context/apm_service/use_service_agent_name_fetcher'; +import * as useServiceAgentNameHook from '../../../context/apm_service/use_service_agent_fetcher'; import { disableConsoleWarning, renderWithTheme, @@ -52,9 +52,10 @@ function setup({ // mock agent jest - .spyOn(useServiceAgentNameHook, 'useServiceAgentNameFetcher') + .spyOn(useServiceAgentNameHook, 'useServiceAgentFetcher') .mockReturnValue({ agentName: 'nodejs', + runtimeName: 'node', error: undefined, status: useFetcherHook.FETCH_STATUS.SUCCESS, }); diff --git a/x-pack/plugins/apm/public/components/routing/templates/apm_service_template/index.test.tsx b/x-pack/plugins/apm/public/components/routing/templates/apm_service_template/index.test.tsx new file mode 100644 index 0000000000000..5a481b2d6f10c --- /dev/null +++ b/x-pack/plugins/apm/public/components/routing/templates/apm_service_template/index.test.tsx @@ -0,0 +1,73 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { isMetricsTabHidden, isJVMsTabHidden } from './'; + +describe('APM service template', () => { + describe('isMetricsTabHidden', () => { + describe('hides metrics tab', () => { + [ + { agentName: undefined }, + { agentName: 'js-base' }, + { agentName: 'rum-js' }, + { agentName: 'opentelemetry/webjs' }, + { agentName: 'java' }, + { agentName: 'opentelemetry/java' }, + { agentName: 'ios/swift' }, + { agentName: 'opentelemetry/swift' }, + { agentName: 'ruby', runtimeName: 'jruby' }, + ].map((input) => { + it(`when input ${JSON.stringify(input)}`, () => { + expect(isMetricsTabHidden(input)).toBeTruthy(); + }); + }); + }); + describe('shows metrics tab', () => { + [ + { agentName: 'ruby', runtimeName: 'ruby' }, + { agentName: 'ruby' }, + { agentName: 'dotnet' }, + { agentName: 'go' }, + { agentName: 'nodejs' }, + { agentName: 'php' }, + { agentName: 'python' }, + ].map((input) => { + it(`when input ${JSON.stringify(input)}`, () => { + expect(isMetricsTabHidden(input)).toBeFalsy(); + }); + }); + }); + }); + describe('isJVMsTabHidden', () => { + describe('hides JVMs tab', () => { + [ + { agentName: undefined }, + { agentName: 'ruby', runtimeName: 'ruby' }, + { agentName: 'ruby' }, + { agentName: 'dotnet' }, + { agentName: 'go' }, + { agentName: 'nodejs' }, + { agentName: 'php' }, + { agentName: 'python' }, + ].map((input) => { + it(`when input ${JSON.stringify(input)}`, () => { + expect(isJVMsTabHidden(input)).toBeTruthy(); + }); + }); + }); + describe('shows JVMs tab', () => { + [ + { agentName: 'java' }, + { agentName: 'opentelemetry/java' }, + { agentName: 'ruby', runtimeName: 'jruby' }, + ].map((input) => { + it(`when input ${JSON.stringify(input)}`, () => { + expect(isJVMsTabHidden(input)).toBeFalsy(); + }); + }); + }); + }); +}); diff --git a/x-pack/plugins/apm/public/components/routing/templates/apm_service_template/index.tsx b/x-pack/plugins/apm/public/components/routing/templates/apm_service_template/index.tsx index efd91fa506c10..c12fdab09613c 100644 --- a/x-pack/plugins/apm/public/components/routing/templates/apm_service_template/index.tsx +++ b/x-pack/plugins/apm/public/components/routing/templates/apm_service_template/index.tsx @@ -18,6 +18,7 @@ import React from 'react'; import { isIosAgentName, isJavaAgentName, + isJRubyAgent, isRumAgentName, } from '../../../../../common/agent_name'; import { useApmPluginContext } from '../../../../context/apm_plugin/use_apm_plugin_context'; @@ -118,8 +119,34 @@ function TemplateWithContext({ ); } +export function isMetricsTabHidden({ + agentName, + runtimeName, +}: { + agentName?: string; + runtimeName?: string; +}) { + return ( + !agentName || + isRumAgentName(agentName) || + isJavaAgentName(agentName) || + isIosAgentName(agentName) || + isJRubyAgent(agentName, runtimeName) + ); +} + +export function isJVMsTabHidden({ + agentName, + runtimeName, +}: { + agentName?: string; + runtimeName?: string; +}) { + return !(isJavaAgentName(agentName) || isJRubyAgent(agentName, runtimeName)); +} + function useTabs({ selectedTab }: { selectedTab: Tab['key'] }) { - const { agentName } = useApmServiceContext(); + const { agentName, runtimeName } = useApmServiceContext(); const { config } = useApmPluginContext(); const router = useApmRouter(); @@ -189,11 +216,7 @@ function useTabs({ selectedTab }: { selectedTab: Tab['key'] }) { label: i18n.translate('xpack.apm.serviceDetails.metricsTabLabel', { defaultMessage: 'Metrics', }), - hidden: - !agentName || - isRumAgentName(agentName) || - isJavaAgentName(agentName) || - isIosAgentName(agentName), + hidden: isMetricsTabHidden({ agentName, runtimeName }), }, { key: 'nodes', @@ -204,7 +227,7 @@ function useTabs({ selectedTab }: { selectedTab: Tab['key'] }) { label: i18n.translate('xpack.apm.serviceDetails.nodesTabLabel', { defaultMessage: 'JVMs', }), - hidden: !isJavaAgentName(agentName), + hidden: isJVMsTabHidden({ agentName, runtimeName }), }, { key: 'service-map', diff --git a/x-pack/plugins/apm/public/components/shared/charts/transaction_distribution_chart/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/transaction_distribution_chart/index.tsx index 51250818a2269..3a4e206957a00 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/transaction_distribution_chart/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/transaction_distribution_chart/index.tsx @@ -22,6 +22,7 @@ import { RecursivePartial, ScaleType, Settings, + LineAnnotationStyle, } from '@elastic/charts'; import euiVars from '@elastic/eui/dist/eui_theme_light.json'; @@ -91,7 +92,7 @@ interface CorrelationsChartProps { selection?: [number, number]; } -const getAnnotationsStyle = (color = 'gray') => ({ +const getAnnotationsStyle = (color = 'gray'): LineAnnotationStyle => ({ line: { strokeWidth: 1, stroke: color, diff --git a/x-pack/plugins/apm/public/context/apm_service/apm_service_context.tsx b/x-pack/plugins/apm/public/context/apm_service/apm_service_context.tsx index 9a66441b8e98b..33bbe390e3c6f 100644 --- a/x-pack/plugins/apm/public/context/apm_service/apm_service_context.tsx +++ b/x-pack/plugins/apm/public/context/apm_service/apm_service_context.tsx @@ -13,7 +13,7 @@ import { TRANSACTION_REQUEST, } from '../../../common/transaction_types'; import { useServiceTransactionTypesFetcher } from './use_service_transaction_types_fetcher'; -import { useServiceAgentNameFetcher } from './use_service_agent_name_fetcher'; +import { useServiceAgentFetcher } from './use_service_agent_fetcher'; import { APIReturnType } from '../../services/rest/createCallApmApi'; import { useServiceAlertsFetcher } from './use_service_alerts_fetcher'; import { useApmParams } from '../../hooks/use_apm_params'; @@ -28,6 +28,7 @@ export const APMServiceContext = createContext<{ transactionType?: string; transactionTypes: string[]; alerts: APMServiceAlert[]; + runtimeName?: string; }>({ serviceName: '', transactionTypes: [], alerts: [] }); export function ApmServiceContextProvider({ @@ -40,7 +41,7 @@ export function ApmServiceContextProvider({ query, } = useApmParams('/services/:serviceName'); - const { agentName } = useServiceAgentNameFetcher(serviceName); + const { agentName, runtimeName } = useServiceAgentFetcher(serviceName); const transactionTypes = useServiceTransactionTypesFetcher(serviceName); @@ -65,6 +66,7 @@ export function ApmServiceContextProvider({ transactionType, transactionTypes, alerts, + runtimeName, }} children={children} /> diff --git a/x-pack/plugins/apm/public/context/apm_service/use_service_agent_name_fetcher.ts b/x-pack/plugins/apm/public/context/apm_service/use_service_agent_fetcher.ts similarity index 70% rename from x-pack/plugins/apm/public/context/apm_service/use_service_agent_name_fetcher.ts rename to x-pack/plugins/apm/public/context/apm_service/use_service_agent_fetcher.ts index 82198eb73b3cb..214b72a34d6e5 100644 --- a/x-pack/plugins/apm/public/context/apm_service/use_service_agent_name_fetcher.ts +++ b/x-pack/plugins/apm/public/context/apm_service/use_service_agent_fetcher.ts @@ -8,14 +8,19 @@ import { useFetcher } from '../../hooks/use_fetcher'; import { useUrlParams } from '../url_params_context/use_url_params'; -export function useServiceAgentNameFetcher(serviceName?: string) { +const INITIAL_STATE = { + agentName: undefined, + runtimeName: undefined, +}; + +export function useServiceAgentFetcher(serviceName?: string) { const { urlParams } = useUrlParams(); const { start, end } = urlParams; - const { data, error, status } = useFetcher( + const { data = INITIAL_STATE, error, status } = useFetcher( (callApmApi) => { if (serviceName && start && end) { return callApmApi({ - endpoint: 'GET /api/apm/services/{serviceName}/agent_name', + endpoint: 'GET /api/apm/services/{serviceName}/agent', params: { path: { serviceName }, query: { start, end }, @@ -26,5 +31,5 @@ export function useServiceAgentNameFetcher(serviceName?: string) { [serviceName, start, end] ); - return { agentName: data?.agentName, status, error }; + return { ...data, status, error }; } diff --git a/x-pack/plugins/apm/server/lib/services/__snapshots__/queries.test.ts.snap b/x-pack/plugins/apm/server/lib/services/__snapshots__/queries.test.ts.snap index be664529abab4..1b5df64dd8d00 100644 --- a/x-pack/plugins/apm/server/lib/services/__snapshots__/queries.test.ts.snap +++ b/x-pack/plugins/apm/server/lib/services/__snapshots__/queries.test.ts.snap @@ -63,14 +63,10 @@ Object { ], }, "body": Object { - "aggs": Object { - "agents": Object { - "terms": Object { - "field": "agent.name", - "size": 1, - }, - }, - }, + "_source": Array [ + "service.runtime.name", + "agent.name", + ], "query": Object { "bool": Object { "filter": Array [ @@ -88,10 +84,20 @@ Object { }, }, }, + Object { + "exists": Object { + "field": "service.runtime.name", + }, + }, + Object { + "exists": Object { + "field": "agent.name", + }, + }, ], }, }, - "size": 0, + "size": 1, }, "terminateAfter": 1, } diff --git a/x-pack/plugins/apm/server/lib/services/get_service_agent_name.ts b/x-pack/plugins/apm/server/lib/services/get_service_agent.ts similarity index 64% rename from x-pack/plugins/apm/server/lib/services/get_service_agent_name.ts rename to x-pack/plugins/apm/server/lib/services/get_service_agent.ts index 49489f2b33888..2a6ec74bc0d1a 100644 --- a/x-pack/plugins/apm/server/lib/services/get_service_agent_name.ts +++ b/x-pack/plugins/apm/server/lib/services/get_service_agent.ts @@ -9,12 +9,24 @@ import { ProcessorEvent } from '../../../common/processor_event'; import { AGENT_NAME, SERVICE_NAME, + SERVICE_RUNTIME_NAME, } from '../../../common/elasticsearch_fieldnames'; import { rangeQuery } from '../../../../observability/server'; import { Setup, SetupTimeRange } from '../helpers/setup_request'; import { getProcessorEventForAggregatedTransactions } from '../helpers/aggregated_transactions'; -export async function getServiceAgentName({ +interface ServiceAgent { + service?: { + runtime: { + name: string; + }; + }; + agent?: { + name: string; + }; +} + +export async function getServiceAgent({ serviceName, setup, searchAggregatedTransactions, @@ -37,27 +49,37 @@ export async function getServiceAgentName({ ], }, body: { - size: 0, + size: 1, + _source: [SERVICE_RUNTIME_NAME, AGENT_NAME], query: { bool: { filter: [ { term: { [SERVICE_NAME]: serviceName } }, ...rangeQuery(start, end), + { + exists: { + field: SERVICE_RUNTIME_NAME, + }, + }, + { + exists: { + field: AGENT_NAME, + }, + }, ], }, }, - aggs: { - agents: { - terms: { field: AGENT_NAME, size: 1 }, - }, - }, }, }; - const { aggregations } = await apmEventClient.search( + const response = await apmEventClient.search( 'get_service_agent_name', params ); - const agentName = aggregations?.agents.buckets[0]?.key as string | undefined; - return { agentName }; + if (response.hits.total.value === 0) { + return {}; + } + + const { service, agent } = response.hits.hits[0]._source as ServiceAgent; + return { agentName: agent?.name, runtimeName: service?.runtime.name }; } diff --git a/x-pack/plugins/apm/server/lib/services/queries.test.ts b/x-pack/plugins/apm/server/lib/services/queries.test.ts index a34382ddaf1fb..be5f280477a09 100644 --- a/x-pack/plugins/apm/server/lib/services/queries.test.ts +++ b/x-pack/plugins/apm/server/lib/services/queries.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { getServiceAgentName } from './get_service_agent_name'; +import { getServiceAgent } from './get_service_agent'; import { getServiceTransactionTypes } from './get_service_transaction_types'; import { getServicesItems } from './get_services/get_services_items'; import { getLegacyDataStatus } from './get_services/get_legacy_data_status'; @@ -25,7 +25,7 @@ describe('services queries', () => { it('fetches the service agent name', async () => { mock = await inspectSearchParams((setup) => - getServiceAgentName({ + getServiceAgent({ serviceName: 'foo', setup, searchAggregatedTransactions: false, diff --git a/x-pack/plugins/apm/server/routes/services.ts b/x-pack/plugins/apm/server/routes/services.ts index b4d185fecf5e2..32a7dcefb5cc8 100644 --- a/x-pack/plugins/apm/server/routes/services.ts +++ b/x-pack/plugins/apm/server/routes/services.ts @@ -16,7 +16,7 @@ import { getThroughputUnit } from '../lib/helpers/calculate_throughput'; import { setupRequest } from '../lib/helpers/setup_request'; import { getServiceAnnotations } from '../lib/services/annotations'; import { getServices } from '../lib/services/get_services'; -import { getServiceAgentName } from '../lib/services/get_service_agent_name'; +import { getServiceAgent } from '../lib/services/get_service_agent'; import { getServiceAlerts } from '../lib/services/get_service_alerts'; import { getServiceDependencies } from '../lib/services/get_service_dependencies'; import { getServiceInstanceMetadataDetails } from '../lib/services/get_service_instance_metadata_details'; @@ -164,8 +164,8 @@ const serviceMetadataIconsRoute = createApmServerRoute({ }, }); -const serviceAgentNameRoute = createApmServerRoute({ - endpoint: 'GET /api/apm/services/{serviceName}/agent_name', +const serviceAgentRoute = createApmServerRoute({ + endpoint: 'GET /api/apm/services/{serviceName}/agent', params: t.type({ path: t.type({ serviceName: t.string, @@ -185,7 +185,7 @@ const serviceAgentNameRoute = createApmServerRoute({ kuery: '', }); - return getServiceAgentName({ + return getServiceAgent({ serviceName, setup, searchAggregatedTransactions, @@ -909,7 +909,7 @@ export const serviceRouteRepository = createApmServerRouteRepository() .add(servicesDetailedStatisticsRoute) .add(serviceMetadataDetailsRoute) .add(serviceMetadataIconsRoute) - .add(serviceAgentNameRoute) + .add(serviceAgentRoute) .add(serviceTransactionTypesRoute) .add(serviceNodeMetadataRoute) .add(serviceAnnotationsRoute) diff --git a/x-pack/plugins/cases/README.md b/x-pack/plugins/cases/README.md index 25113ccbb30df..f894ca23dfbf0 100644 --- a/x-pack/plugins/cases/README.md +++ b/x-pack/plugins/cases/README.md @@ -43,6 +43,16 @@ cases: CasesUiStart; cases.getCreateCase({ onCancel: handleSetIsCancel, onSuccess, + lensIntegration?: { + plugins: { + parsingPlugin, + processingPluginRenderer, + uiPlugin, + }, + hooks: { + useInsertTimeline, + }, + } timelineIntegration?: { plugins: { parsingPlugin, diff --git a/x-pack/plugins/cases/common/ui/types.ts b/x-pack/plugins/cases/common/ui/types.ts index 3edbd3443ffc1..bf4ec0da6ee56 100644 --- a/x-pack/plugins/cases/common/ui/types.ts +++ b/x-pack/plugins/cases/common/ui/types.ts @@ -18,6 +18,12 @@ import { UserActionField, } from '../api'; +export interface CasesUiConfigType { + markdownPlugins: { + lens: boolean; + }; +} + export const StatusAll = 'all' as const; export type StatusAllType = typeof StatusAll; diff --git a/x-pack/plugins/cases/common/utils/markdown_plugins/lens/constants.ts b/x-pack/plugins/cases/common/utils/markdown_plugins/lens/constants.ts new file mode 100644 index 0000000000000..bc67e1b3228bb --- /dev/null +++ b/x-pack/plugins/cases/common/utils/markdown_plugins/lens/constants.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 const LENS_ID = 'lens'; diff --git a/x-pack/plugins/cases/common/utils/markdown_plugins/lens/index.ts b/x-pack/plugins/cases/common/utils/markdown_plugins/lens/index.ts new file mode 100644 index 0000000000000..4f48da5838380 --- /dev/null +++ b/x-pack/plugins/cases/common/utils/markdown_plugins/lens/index.ts @@ -0,0 +1,10 @@ +/* + * 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 './constants'; +export * from './parser'; +export * from './serializer'; diff --git a/x-pack/plugins/cases/common/utils/markdown_plugins/lens/parser.ts b/x-pack/plugins/cases/common/utils/markdown_plugins/lens/parser.ts new file mode 100644 index 0000000000000..58ebfd76d5ac5 --- /dev/null +++ b/x-pack/plugins/cases/common/utils/markdown_plugins/lens/parser.ts @@ -0,0 +1,77 @@ +/* + * 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 { Plugin } from 'unified'; +import { RemarkTokenizer } from '@elastic/eui'; +import { LENS_ID } from './constants'; + +export const LensParser: Plugin = function () { + const Parser = this.Parser; + const tokenizers = Parser.prototype.blockTokenizers; + const methods = Parser.prototype.blockMethods; + + const tokenizeLens: RemarkTokenizer = function (eat, value, silent) { + if (value.startsWith(`!{${LENS_ID}`) === false) return true; + + const nextChar = value[6]; + + if (nextChar !== '{' && nextChar !== '}') return false; // this isn't actually a lens + + if (silent) { + return true; + } + + // is there a configuration? + const hasConfiguration = nextChar === '{'; + + let match = `!{${LENS_ID}`; + let configuration = {}; + + if (hasConfiguration) { + let configurationString = ''; + + let openObjects = 0; + + for (let i = 6; i < value.length; i++) { + const char = value[i]; + if (char === '{') { + openObjects++; + configurationString += char; + } else if (char === '}') { + openObjects--; + if (openObjects === -1) { + break; + } + configurationString += char; + } else { + configurationString += char; + } + } + + match += configurationString; + try { + configuration = JSON.parse(configurationString); + } catch (e) { + const now = eat.now(); + this.file.fail(`Unable to parse lens JSON configuration: ${e}`, { + line: now.line, + column: now.column + 6, + }); + } + } + + match += '}'; + + return eat(match)({ + type: LENS_ID, + ...configuration, + }); + }; + + tokenizers.lens = tokenizeLens; + methods.splice(methods.indexOf('text'), 0, LENS_ID); +}; diff --git a/x-pack/plugins/cases/common/utils/markdown_plugins/lens/serializer.ts b/x-pack/plugins/cases/common/utils/markdown_plugins/lens/serializer.ts new file mode 100644 index 0000000000000..e561b2f8cfb8a --- /dev/null +++ b/x-pack/plugins/cases/common/utils/markdown_plugins/lens/serializer.ts @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { TimeRange } from 'src/plugins/data/common'; +import { LENS_ID } from './constants'; + +export interface LensSerializerProps { + attributes: Record; + timeRange: TimeRange; +} + +export const LensSerializer = ({ timeRange, attributes }: LensSerializerProps) => + `!{${LENS_ID}${JSON.stringify({ + timeRange, + attributes, + })}}`; diff --git a/x-pack/plugins/cases/common/utils/markdown_plugins/timeline/index.ts b/x-pack/plugins/cases/common/utils/markdown_plugins/timeline/index.ts new file mode 100644 index 0000000000000..c6a22791db5f6 --- /dev/null +++ b/x-pack/plugins/cases/common/utils/markdown_plugins/timeline/index.ts @@ -0,0 +1,9 @@ +/* + * 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 './parser'; +export * from './serializer'; diff --git a/x-pack/plugins/cases/common/utils/markdown_plugins/timeline/parser.ts b/x-pack/plugins/cases/common/utils/markdown_plugins/timeline/parser.ts new file mode 100644 index 0000000000000..0decdae8c7348 --- /dev/null +++ b/x-pack/plugins/cases/common/utils/markdown_plugins/timeline/parser.ts @@ -0,0 +1,83 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { Plugin } from 'unified'; +import { RemarkTokenizer } from '@elastic/eui'; +import * as i18n from './translations'; + +export const ID = 'timeline'; +const PREFIX = '['; + +export const TimelineParser: Plugin = function () { + const Parser = this.Parser; + const tokenizers = Parser.prototype.blockTokenizers; + const methods = Parser.prototype.blockMethods; + + const tokenizeTimeline: RemarkTokenizer = function (eat, value, silent) { + if ( + value.startsWith(PREFIX) === false || + (value.startsWith(PREFIX) === true && !value.includes('timelines?timeline=(id')) + ) { + return false; + } + + let index = 0; + const nextChar = value[index]; + + if (nextChar !== PREFIX) { + return false; + } + + if (silent) { + return true; + } + + function readArg(open: string, close: string) { + if (value[index] !== open) { + throw new Error(i18n.NO_PARENTHESES); + } + + index++; + + let body = ''; + let openBrackets = 0; + + for (; index < value.length; index++) { + const char = value[index]; + + if (char === close && openBrackets === 0) { + index++; + return body; + } else if (char === close) { + openBrackets--; + } else if (char === open) { + openBrackets++; + } + + body += char; + } + + return ''; + } + + const timelineTitle = readArg(PREFIX, ']'); + const timelineUrl = readArg('(', ')'); + const match = `[${timelineTitle}](${timelineUrl})`; + + return eat(match)({ + type: ID, + match, + }); + }; + + tokenizeTimeline.locator = (value: string, fromIndex: number) => { + return value.indexOf(PREFIX, fromIndex); + }; + + tokenizers.timeline = tokenizeTimeline; + methods.splice(methods.indexOf('url'), 0, ID); +}; diff --git a/x-pack/plugins/cases/common/utils/markdown_plugins/timeline/serializer.ts b/x-pack/plugins/cases/common/utils/markdown_plugins/timeline/serializer.ts new file mode 100644 index 0000000000000..0a95c9466b1ff --- /dev/null +++ b/x-pack/plugins/cases/common/utils/markdown_plugins/timeline/serializer.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export interface TimelineSerializerProps { + match: string; +} + +export const TimelineSerializer = ({ match }: TimelineSerializerProps) => match; diff --git a/x-pack/plugins/cases/common/utils/markdown_plugins/timeline/translations.ts b/x-pack/plugins/cases/common/utils/markdown_plugins/timeline/translations.ts new file mode 100644 index 0000000000000..a1244f0ae67aa --- /dev/null +++ b/x-pack/plugins/cases/common/utils/markdown_plugins/timeline/translations.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; + +export const NO_PARENTHESES = i18n.translate( + 'xpack.cases.markdownEditor.plugins.timeline.noParenthesesErrorMsg', + { + defaultMessage: 'Expected left parentheses', + } +); diff --git a/x-pack/plugins/cases/kibana.json b/x-pack/plugins/cases/kibana.json index f72f0e012bd80..ebac6295166df 100644 --- a/x-pack/plugins/cases/kibana.json +++ b/x-pack/plugins/cases/kibana.json @@ -20,11 +20,15 @@ "requiredPlugins":[ "actions", "esUiShared", + "lens", "features", "kibanaReact", "kibanaUtils", "triggersActionsUi" ], + "requiredBundles": [ + "savedObjects" + ], "server":true, "ui":true, "version":"8.0.0" diff --git a/x-pack/plugins/cases/public/common/lib/kibana/__mocks__/index.ts b/x-pack/plugins/cases/public/common/lib/kibana/__mocks__/index.ts index 392b71befe2b4..fb5e3f89d74b1 100644 --- a/x-pack/plugins/cases/public/common/lib/kibana/__mocks__/index.ts +++ b/x-pack/plugins/cases/public/common/lib/kibana/__mocks__/index.ts @@ -12,7 +12,11 @@ import { createWithKibanaMock, } from '../kibana_react.mock'; -export const KibanaServices = { get: jest.fn(), getKibanaVersion: jest.fn(() => '8.0.0') }; +export const KibanaServices = { + get: jest.fn(), + getKibanaVersion: jest.fn(() => '8.0.0'), + getConfig: jest.fn(() => null), +}; export const useKibana = jest.fn().mockReturnValue({ services: createStartServicesMock(), }); diff --git a/x-pack/plugins/cases/public/common/lib/kibana/kibana_react.mock.ts b/x-pack/plugins/cases/public/common/lib/kibana/kibana_react.mock.ts index ff03782447846..e1990efefeffc 100644 --- a/x-pack/plugins/cases/public/common/lib/kibana/kibana_react.mock.ts +++ b/x-pack/plugins/cases/public/common/lib/kibana/kibana_react.mock.ts @@ -15,6 +15,13 @@ import { EuiTheme } from '../../../../../../../src/plugins/kibana_react/common'; import { securityMock } from '../../../../../security/public/mocks'; import { triggersActionsUiMock } from '../../../../../triggers_actions_ui/public/mocks'; +export const mockCreateStartServicesMock = (): StartServices => + (({ + ...coreMock.createStart(), + security: securityMock.createStart(), + triggersActionsUi: triggersActionsUiMock.createStart(), + } as unknown) as StartServices); + export const createStartServicesMock = (): StartServices => (({ ...coreMock.createStart(), diff --git a/x-pack/plugins/cases/public/common/lib/kibana/services.ts b/x-pack/plugins/cases/public/common/lib/kibana/services.ts index 94487bd3ca5e9..3a1f220d9794f 100644 --- a/x-pack/plugins/cases/public/common/lib/kibana/services.ts +++ b/x-pack/plugins/cases/public/common/lib/kibana/services.ts @@ -6,16 +6,23 @@ */ import { CoreStart } from 'kibana/public'; +import { CasesUiConfigType } from '../../../../common/ui/types'; type GlobalServices = Pick; export class KibanaServices { private static kibanaVersion?: string; private static services?: GlobalServices; + private static config?: CasesUiConfigType; - public static init({ http, kibanaVersion }: GlobalServices & { kibanaVersion: string }) { + public static init({ + http, + kibanaVersion, + config, + }: GlobalServices & { kibanaVersion: string; config: CasesUiConfigType }) { this.services = { http }; this.kibanaVersion = kibanaVersion; + this.config = config; } public static get(): GlobalServices { @@ -34,6 +41,10 @@ export class KibanaServices { return this.kibanaVersion; } + public static getConfig() { + return this.config; + } + private static throwUninitializedError(): never { throw new Error( 'Kibana services not initialized - are you trying to import this module from outside of the Cases app?' diff --git a/x-pack/plugins/cases/public/components/add_comment/index.test.tsx b/x-pack/plugins/cases/public/components/add_comment/index.test.tsx index db3f22a074d3b..06a3897687921 100644 --- a/x-pack/plugins/cases/public/components/add_comment/index.test.tsx +++ b/x-pack/plugins/cases/public/components/add_comment/index.test.tsx @@ -26,6 +26,7 @@ const onCommentPosted = jest.fn(); const postComment = jest.fn(); const addCommentProps: AddCommentProps = { + id: 'newComment', caseId: '1234', userCanCrud: true, onCommentSaving, diff --git a/x-pack/plugins/cases/public/components/add_comment/index.tsx b/x-pack/plugins/cases/public/components/add_comment/index.tsx index 4ec06d6b55197..f788456a30dff 100644 --- a/x-pack/plugins/cases/public/components/add_comment/index.tsx +++ b/x-pack/plugins/cases/public/components/add_comment/index.tsx @@ -6,7 +6,7 @@ */ import { EuiButton, EuiFlexItem, EuiFlexGroup, EuiLoadingSpinner } from '@elastic/eui'; -import React, { useCallback, forwardRef, useImperativeHandle } from 'react'; +import React, { useCallback, useRef, forwardRef, useImperativeHandle } from 'react'; import styled from 'styled-components'; import { CommentType } from '../../../common'; @@ -19,6 +19,7 @@ import * as i18n from './translations'; import { schema, AddCommentFormSchema } from './schema'; import { InsertTimeline } from '../insert_timeline'; import { useOwnerContext } from '../owner_context/use_owner_context'; + const MySpinner = styled(EuiLoadingSpinner)` position: absolute; top: 50%; @@ -31,9 +32,11 @@ const initialCommentValue: AddCommentFormSchema = { export interface AddCommentRefObject { addQuote: (quote: string) => void; + setComment: (newComment: string) => void; } export interface AddCommentProps { + id: string; caseId: string; userCanCrud?: boolean; onCommentSaving?: () => void; @@ -47,6 +50,7 @@ export const AddComment = React.memo( forwardRef( ( { + id, caseId, userCanCrud, onCommentPosted, @@ -57,6 +61,7 @@ export const AddComment = React.memo( }, ref ) => { + const editorRef = useRef(); const owner = useOwnerContext(); const { isLoading, postComment } = usePostComment(); @@ -77,8 +82,17 @@ export const AddComment = React.memo( [comment, setFieldValue] ); + const setComment = useCallback( + (newComment) => { + setFieldValue(fieldName, newComment); + }, + [setFieldValue] + ); + useImperativeHandle(ref, () => ({ addQuote, + setComment, + editor: editorRef.current, })); const onSubmit = useCallback(async () => { @@ -106,6 +120,8 @@ export const AddComment = React.memo( path={fieldName} component={MarkdownEditorForm} componentProps={{ + ref: editorRef, + id, idAria: 'caseComment', isDisabled: isLoading, dataTestSubj: 'add-comment', diff --git a/x-pack/plugins/cases/public/components/create/description.test.tsx b/x-pack/plugins/cases/public/components/create/description.test.tsx index fcd1f82d64a53..923c73193f992 100644 --- a/x-pack/plugins/cases/public/components/create/description.test.tsx +++ b/x-pack/plugins/cases/public/components/create/description.test.tsx @@ -12,6 +12,7 @@ import { act } from '@testing-library/react'; import { useForm, Form, FormHook } from '../../common/shared_imports'; import { Description } from './description'; import { schema, FormProps } from './schema'; +jest.mock('../markdown_editor/plugins/lens/use_lens_draft_comment'); describe('Description', () => { let globalForm: FormHook; diff --git a/x-pack/plugins/cases/public/components/create/description.tsx b/x-pack/plugins/cases/public/components/create/description.tsx index 0a7102cff1ad5..d11c64789c3f0 100644 --- a/x-pack/plugins/cases/public/components/create/description.tsx +++ b/x-pack/plugins/cases/public/components/create/description.tsx @@ -5,26 +5,43 @@ * 2.0. */ -import React, { memo } from 'react'; +import React, { memo, useEffect, useRef } from 'react'; import { MarkdownEditorForm } from '../markdown_editor'; -import { UseField } from '../../common/shared_imports'; +import { UseField, useFormContext } from '../../common/shared_imports'; +import { useLensDraftComment } from '../markdown_editor/plugins/lens/use_lens_draft_comment'; + interface Props { isLoading: boolean; } export const fieldName = 'description'; -const DescriptionComponent: React.FC = ({ isLoading }) => ( - -); +const DescriptionComponent: React.FC = ({ isLoading }) => { + const { draftComment, openLensModal } = useLensDraftComment(); + const { setFieldValue } = useFormContext(); + const editorRef = useRef>(); + + useEffect(() => { + if (draftComment?.commentId === fieldName && editorRef.current) { + setFieldValue(fieldName, draftComment.comment); + openLensModal({ editorRef: editorRef.current }); + } + }, [draftComment, openLensModal, setFieldValue]); + + return ( + + ); +}; DescriptionComponent.displayName = 'DescriptionComponent'; diff --git a/x-pack/plugins/cases/public/components/create/form.test.tsx b/x-pack/plugins/cases/public/components/create/form.test.tsx index 783ead9b271fd..9c3071fe27ee5 100644 --- a/x-pack/plugins/cases/public/components/create/form.test.tsx +++ b/x-pack/plugins/cases/public/components/create/form.test.tsx @@ -23,6 +23,7 @@ import { useCaseConfigureResponse } from '../configure_cases/__mock__'; jest.mock('../../containers/use_get_tags'); jest.mock('../../containers/configure/use_connectors'); jest.mock('../../containers/configure/use_configure'); +jest.mock('../markdown_editor/plugins/lens/use_lens_draft_comment'); const useGetTagsMock = useGetTags as jest.Mock; const useConnectorsMock = useConnectors as jest.Mock; diff --git a/x-pack/plugins/cases/public/components/markdown_editor/context.tsx b/x-pack/plugins/cases/public/components/markdown_editor/context.tsx new file mode 100644 index 0000000000000..d7f5b0612cb73 --- /dev/null +++ b/x-pack/plugins/cases/public/components/markdown_editor/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 React from 'react'; + +export const CommentEditorContext = React.createContext<{ + editorId: string; + value: string; +} | null>(null); diff --git a/x-pack/plugins/cases/public/components/markdown_editor/editor.tsx b/x-pack/plugins/cases/public/components/markdown_editor/editor.tsx index 4bd26678e41a2..64aac233f1bb9 100644 --- a/x-pack/plugins/cases/public/components/markdown_editor/editor.tsx +++ b/x-pack/plugins/cases/public/components/markdown_editor/editor.tsx @@ -5,15 +5,26 @@ * 2.0. */ -import React, { memo, useState, useCallback } from 'react'; +import React, { + memo, + forwardRef, + useCallback, + useMemo, + useRef, + useState, + useImperativeHandle, + ElementRef, +} from 'react'; import { PluggableList } from 'unified'; import { EuiMarkdownEditor, EuiMarkdownEditorUiPlugin } from '@elastic/eui'; +import { ContextShape } from '@elastic/eui/src/components/markdown_editor/markdown_context'; import { usePlugins } from './use_plugins'; +import { CommentEditorContext } from './context'; interface MarkdownEditorProps { ariaLabel: string; dataTestSubj?: string; - editorId?: string; + editorId: string; height?: number; onChange: (content: string) => void; parsingPlugins?: PluggableList; @@ -22,35 +33,64 @@ interface MarkdownEditorProps { value: string; } -const MarkdownEditorComponent: React.FC = ({ - ariaLabel, - dataTestSubj, - editorId, - height, - onChange, - value, -}) => { - const [markdownErrorMessages, setMarkdownErrorMessages] = useState([]); - const onParse = useCallback((err, { messages }) => { - setMarkdownErrorMessages(err ? [err] : messages); - }, []); - const { parsingPlugins, processingPlugins, uiPlugins } = usePlugins(); - - return ( - - ); -}; +type EuiMarkdownEditorRef = ElementRef; + +export interface MarkdownEditorRef { + textarea: HTMLTextAreaElement | null; + replaceNode: ContextShape['replaceNode']; + toolbar: HTMLDivElement | null; +} + +const MarkdownEditorComponent = forwardRef( + ({ ariaLabel, dataTestSubj, editorId, height, onChange, value }, ref) => { + const [markdownErrorMessages, setMarkdownErrorMessages] = useState([]); + const onParse = useCallback((err, { messages }) => { + setMarkdownErrorMessages(err ? [err] : messages); + }, []); + const { parsingPlugins, processingPlugins, uiPlugins } = usePlugins(); + const editorRef = useRef(null); + + const commentEditorContextValue = useMemo( + () => ({ + editorId, + value, + }), + [editorId, value] + ); + + // @ts-expect-error + useImperativeHandle(ref, () => { + if (!editorRef.current) { + return null; + } + + const editorNode = editorRef.current?.textarea?.closest('.euiMarkdownEditor'); + + return { + ...editorRef.current, + toolbar: editorNode?.querySelector('.euiMarkdownEditorToolbar'), + }; + }); + + return ( + + + + ); + } +); export const MarkdownEditor = memo(MarkdownEditorComponent); diff --git a/x-pack/plugins/cases/public/components/markdown_editor/eui_form.tsx b/x-pack/plugins/cases/public/components/markdown_editor/eui_form.tsx index c2b2e8c77cb38..2719f38f98fc2 100644 --- a/x-pack/plugins/cases/public/components/markdown_editor/eui_form.tsx +++ b/x-pack/plugins/cases/public/components/markdown_editor/eui_form.tsx @@ -5,11 +5,11 @@ * 2.0. */ -import React from 'react'; +import React, { forwardRef } from 'react'; import styled from 'styled-components'; import { EuiMarkdownEditorProps, EuiFormRow, EuiFlexItem, EuiFlexGroup } from '@elastic/eui'; import { FieldHook, getFieldValidityAndErrorMessage } from '../../common/shared_imports'; -import { MarkdownEditor } from './editor'; +import { MarkdownEditor, MarkdownEditorRef } from './editor'; type MarkdownEditorFormProps = EuiMarkdownEditorProps & { id: string; @@ -26,40 +26,39 @@ const BottomContentWrapper = styled(EuiFlexGroup)` `} `; -export const MarkdownEditorForm: React.FC = ({ - id, - field, - dataTestSubj, - idAria, - bottomRightContent, -}) => { - const { isInvalid, errorMessage } = getFieldValidityAndErrorMessage(field); +export const MarkdownEditorForm = React.memo( + forwardRef( + ({ id, field, dataTestSubj, idAria, bottomRightContent }, ref) => { + const { isInvalid, errorMessage } = getFieldValidityAndErrorMessage(field); - return ( - <> - - - - {bottomRightContent && ( - - {bottomRightContent} - - )} - - ); -}; + return ( + <> + + + + {bottomRightContent && ( + + {bottomRightContent} + + )} + + ); + } + ) +); diff --git a/x-pack/plugins/cases/public/components/markdown_editor/plugins/lens/__mocks__/use_lens_draft_comment.ts b/x-pack/plugins/cases/public/components/markdown_editor/plugins/lens/__mocks__/use_lens_draft_comment.ts new file mode 100644 index 0000000000000..a0f0d49b211fb --- /dev/null +++ b/x-pack/plugins/cases/public/components/markdown_editor/plugins/lens/__mocks__/use_lens_draft_comment.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 const useLensDraftComment = () => ({}); diff --git a/x-pack/plugins/cases/public/components/markdown_editor/plugins/lens/constants.ts b/x-pack/plugins/cases/public/components/markdown_editor/plugins/lens/constants.ts new file mode 100644 index 0000000000000..05826f73fe007 --- /dev/null +++ b/x-pack/plugins/cases/public/components/markdown_editor/plugins/lens/constants.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export const ID = 'lens'; +export const PREFIX = `[`; +export const LENS_VISUALIZATION_HEIGHT = 200; +export const DRAFT_COMMENT_STORAGE_ID = 'xpack.cases.commentDraft'; diff --git a/x-pack/plugins/cases/public/components/markdown_editor/plugins/lens/index.ts b/x-pack/plugins/cases/public/components/markdown_editor/plugins/lens/index.ts new file mode 100644 index 0000000000000..1d0bb2bf6c86e --- /dev/null +++ b/x-pack/plugins/cases/public/components/markdown_editor/plugins/lens/index.ts @@ -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 { plugin } from './plugin'; +import { LensParser } from './parser'; +import { LensMarkDownRenderer } from './processor'; +import { INSERT_LENS } from './translations'; + +export { plugin, LensParser as parser, LensMarkDownRenderer as renderer, INSERT_LENS }; diff --git a/x-pack/plugins/cases/public/components/markdown_editor/plugins/lens/modal_container.tsx b/x-pack/plugins/cases/public/components/markdown_editor/plugins/lens/modal_container.tsx new file mode 100644 index 0000000000000..0f70e80deed41 --- /dev/null +++ b/x-pack/plugins/cases/public/components/markdown_editor/plugins/lens/modal_container.tsx @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import styled from 'styled-components'; + +export const ModalContainer = styled.div` + width: ${({ theme }) => theme.eui.euiBreakpoints.m}; + + .euiModalBody { + min-height: 300px; + } +`; diff --git a/x-pack/plugins/cases/public/components/markdown_editor/plugins/lens/parser.ts b/x-pack/plugins/cases/public/components/markdown_editor/plugins/lens/parser.ts new file mode 100644 index 0000000000000..8d598fad260dc --- /dev/null +++ b/x-pack/plugins/cases/public/components/markdown_editor/plugins/lens/parser.ts @@ -0,0 +1,77 @@ +/* + * 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 { Plugin } from 'unified'; +import { RemarkTokenizer } from '@elastic/eui'; +import { ID } from './constants'; + +export const LensParser: Plugin = function () { + const Parser = this.Parser; + const tokenizers = Parser.prototype.blockTokenizers; + const methods = Parser.prototype.blockMethods; + + const tokenizeLens: RemarkTokenizer = function (eat, value, silent) { + if (value.startsWith(`!{${ID}`) === false) return false; + + const nextChar = value[6]; + + if (nextChar !== '{' && nextChar !== '}') return false; // this isn't actually a lens + + if (silent) { + return true; + } + + // is there a configuration? + const hasConfiguration = nextChar === '{'; + + let match = `!{${ID}`; + let configuration = {}; + + if (hasConfiguration) { + let configurationString = ''; + + let openObjects = 0; + + for (let i = 6; i < value.length; i++) { + const char = value[i]; + if (char === '{') { + openObjects++; + configurationString += char; + } else if (char === '}') { + openObjects--; + if (openObjects === -1) { + break; + } + configurationString += char; + } else { + configurationString += char; + } + } + + match += configurationString; + try { + configuration = JSON.parse(configurationString); + } catch (e) { + const now = eat.now(); + this.file.fail(`Unable to parse lens JSON configuration: ${e}`, { + line: now.line, + column: now.column + 6, + }); + } + } + + match += '}'; + + return eat(match)({ + type: ID, + ...configuration, + }); + }; + + tokenizers.lens = tokenizeLens; + methods.splice(methods.indexOf('text'), 0, ID); +}; diff --git a/x-pack/plugins/cases/public/components/markdown_editor/plugins/lens/plugin.tsx b/x-pack/plugins/cases/public/components/markdown_editor/plugins/lens/plugin.tsx new file mode 100644 index 0000000000000..24dde054d2d19 --- /dev/null +++ b/x-pack/plugins/cases/public/components/markdown_editor/plugins/lens/plugin.tsx @@ -0,0 +1,464 @@ +/* + * 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 { first } from 'rxjs/operators'; +import { + EuiFieldText, + EuiModalBody, + EuiModalHeader, + EuiModalHeaderTitle, + EuiMarkdownEditorUiPlugin, + EuiMarkdownContext, + EuiCodeBlock, + EuiSpacer, + EuiModalFooter, + EuiButtonEmpty, + EuiButton, + EuiFlexItem, + EuiFlexGroup, + EuiFormRow, + EuiMarkdownAstNodePosition, + EuiBetaBadge, +} from '@elastic/eui'; +import React, { ReactNode, useCallback, useContext, useMemo, useEffect, useState } from 'react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { useLocation } from 'react-router-dom'; +import styled from 'styled-components'; + +import type { TypedLensByValueInput } from '../../../../../../lens/public'; +import { useKibana } from '../../../../common/lib/kibana'; +import { LensMarkDownRenderer } from './processor'; +import { DRAFT_COMMENT_STORAGE_ID, ID } from './constants'; +import { CommentEditorContext } from '../../context'; +import { ModalContainer } from './modal_container'; +import type { EmbeddablePackageState } from '../../../../../../../../src/plugins/embeddable/public'; +import { + SavedObjectFinderUi, + SavedObjectFinderUiProps, +} from '../../../../../../../../src/plugins/saved_objects/public'; +import { useLensDraftComment } from './use_lens_draft_comment'; + +const BetaBadgeWrapper = styled.span` + display: inline-flex; + + .euiToolTipAnchor { + display: inline-flex; + } +`; + +type LensIncomingEmbeddablePackage = Omit & { + input: TypedLensByValueInput; +}; + +type LensEuiMarkdownEditorUiPlugin = EuiMarkdownEditorUiPlugin<{ + title: string; + timeRange: TypedLensByValueInput['timeRange']; + startDate: string; + endDate: string; + position: EuiMarkdownAstNodePosition; + attributes: TypedLensByValueInput['attributes']; +}>; + +interface LensSavedObjectsPickerProps { + children: ReactNode; + onChoose: SavedObjectFinderUiProps['onChoose']; +} + +const LensSavedObjectsPickerComponent: React.FC = ({ + children, + onChoose, +}) => { + const { savedObjects, uiSettings } = useKibana().services; + + const savedObjectMetaData = useMemo( + () => [ + { + type: 'lens', + getIconForSavedObject: () => 'lensApp', + name: i18n.translate( + 'xpack.cases.markdownEditor.plugins.lens.insertLensSavedObjectModal.searchSelection.savedObjectType.lens', + { + defaultMessage: 'Lens', + } + ), + includeFields: ['*'], + }, + ], + [] + ); + + return ( + + } + savedObjectMetaData={savedObjectMetaData} + fixedPageSize={10} + uiSettings={uiSettings} + savedObjects={savedObjects} + children={children} + /> + ); +}; + +export const LensSavedObjectsPicker = React.memo(LensSavedObjectsPickerComponent); + +const LensEditorComponent: LensEuiMarkdownEditorUiPlugin['editor'] = ({ + node, + onCancel, + onSave, +}) => { + const location = useLocation(); + const { + application: { currentAppId$ }, + embeddable, + lens, + storage, + data: { + query: { + timefilter: { timefilter }, + }, + }, + } = useKibana().services; + const [currentAppId, setCurrentAppId] = useState(undefined); + + const { draftComment, clearDraftComment } = useLensDraftComment(); + + const [nodePosition, setNodePosition] = useState( + undefined + ); + // const [editMode, setEditMode] = useState(!!node); + const [lensEmbeddableAttributes, setLensEmbeddableAttributes] = useState< + TypedLensByValueInput['attributes'] | null + >(node?.attributes || null); + const [timeRange, setTimeRange] = useState( + node?.timeRange ?? { + from: 'now-7d', + to: 'now', + mode: 'relative', + } + ); + const commentEditorContext = useContext(CommentEditorContext); + const markdownContext = useContext(EuiMarkdownContext); + + const handleTitleChange = useCallback((e) => { + const title = e.target.value ?? ''; + setLensEmbeddableAttributes((currentValue) => { + if (currentValue) { + return { ...currentValue, title } as TypedLensByValueInput['attributes']; + } + + return currentValue; + }); + }, []); + + const handleClose = useCallback(() => { + if (currentAppId) { + embeddable?.getStateTransfer().getIncomingEmbeddablePackage(currentAppId, true); + clearDraftComment(); + } + onCancel(); + }, [clearDraftComment, currentAppId, embeddable, onCancel]); + + const handleAdd = useCallback(() => { + if (nodePosition) { + markdownContext.replaceNode( + nodePosition, + `!{${ID}${JSON.stringify({ + timeRange, + attributes: lensEmbeddableAttributes, + })}}` + ); + + handleClose(); + return; + } + + if (lensEmbeddableAttributes) { + onSave( + `!{${ID}${JSON.stringify({ + timeRange, + attributes: lensEmbeddableAttributes, + })}}`, + { + block: true, + } + ); + } + + handleClose(); + }, [nodePosition, lensEmbeddableAttributes, handleClose, markdownContext, timeRange, onSave]); + + const handleDelete = useCallback(() => { + if (nodePosition) { + markdownContext.replaceNode(nodePosition, ``); + onCancel(); + } + }, [markdownContext, nodePosition, onCancel]); + + const originatingPath = useMemo(() => `${location.pathname}${location.search}`, [ + location.pathname, + location.search, + ]); + + const handleEditInLensClick = useCallback( + async (lensAttributes?) => { + storage.set(DRAFT_COMMENT_STORAGE_ID, { + commentId: commentEditorContext?.editorId, + comment: commentEditorContext?.value, + position: node?.position, + title: lensEmbeddableAttributes?.title, + }); + + lens?.navigateToPrefilledEditor( + lensAttributes || lensEmbeddableAttributes + ? { + id: '', + timeRange, + attributes: lensAttributes ?? lensEmbeddableAttributes, + } + : undefined, + { + originatingApp: currentAppId!, + originatingPath, + } + ); + }, + [ + storage, + commentEditorContext?.editorId, + commentEditorContext?.value, + node?.position, + lens, + lensEmbeddableAttributes, + timeRange, + currentAppId, + originatingPath, + ] + ); + + const handleChooseLensSO = useCallback( + (savedObjectId, savedObjectType, fullName, savedObject) => { + handleEditInLensClick({ + ...savedObject.attributes, + title: '', + references: savedObject.references, + }); + }, + [handleEditInLensClick] + ); + + useEffect(() => { + if (node?.attributes) { + setLensEmbeddableAttributes(node.attributes); + } + }, [node?.attributes]); + + useEffect(() => { + const position = node?.position || draftComment?.position; + if (position) { + setNodePosition(position); + } + }, [node?.position, draftComment?.position]); + + useEffect(() => { + const getCurrentAppId = async () => { + const appId = await currentAppId$.pipe(first()).toPromise(); + setCurrentAppId(appId); + }; + getCurrentAppId(); + }, [currentAppId$]); + + useEffect(() => { + let incomingEmbeddablePackage; + + if (currentAppId) { + incomingEmbeddablePackage = embeddable + ?.getStateTransfer() + .getIncomingEmbeddablePackage(currentAppId, true) as LensIncomingEmbeddablePackage; + } + + if ( + incomingEmbeddablePackage?.type === 'lens' && + incomingEmbeddablePackage?.input?.attributes + ) { + const attributesTitle = incomingEmbeddablePackage?.input.attributes.title.length + ? incomingEmbeddablePackage?.input.attributes.title + : null; + setLensEmbeddableAttributes({ + ...incomingEmbeddablePackage?.input.attributes, + title: attributesTitle ?? draftComment?.title ?? '', + }); + + const lensTime = timefilter.getTime(); + if (lensTime?.from && lensTime?.to) { + setTimeRange({ + from: lensTime.from, + to: lensTime.to, + mode: [lensTime.from, lensTime.to].join('').includes('now') ? 'relative' : 'absolute', + }); + } + } + }, [embeddable, storage, timefilter, currentAppId, draftComment?.title]); + + return ( + + + + + + {!!nodePosition ? ( + + ) : ( + + )} + + + + + + + + + + + {lensEmbeddableAttributes ? ( + <> + + + + + + + + + + + + + + + + ) : ( + + + + + + + + )} + + + + + + {!!nodePosition ? ( + + + + ) : null} + + {!!nodePosition ? ( + + ) : ( + + )} + + + + ); +}; + +export const LensEditor = React.memo(LensEditorComponent); + +export const plugin: LensEuiMarkdownEditorUiPlugin = { + name: ID, + button: { + label: i18n.translate('xpack.cases.markdownEditor.plugins.lens.insertLensButtonLabel', { + defaultMessage: 'Insert visualization', + }), + iconType: 'lensApp', + }, + helpText: ( + + {'!{lens}'} + + ), + editor: LensEditor, +}; diff --git a/x-pack/plugins/cases/public/components/markdown_editor/plugins/lens/processor.tsx b/x-pack/plugins/cases/public/components/markdown_editor/plugins/lens/processor.tsx new file mode 100644 index 0000000000000..cc8ef07392670 --- /dev/null +++ b/x-pack/plugins/cases/public/components/markdown_editor/plugins/lens/processor.tsx @@ -0,0 +1,129 @@ +/* + * 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 { first } from 'rxjs/operators'; +import React, { useCallback, useEffect, useState } from 'react'; +import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiText, EuiSpacer } from '@elastic/eui'; +import styled from 'styled-components'; +import { useLocation } from 'react-router-dom'; + +import { createGlobalStyle } from '../../../../../../../../src/plugins/kibana_react/common'; +import { TypedLensByValueInput } from '../../../../../../lens/public'; +import { useKibana } from '../../../../common/lib/kibana'; +import { LENS_VISUALIZATION_HEIGHT } from './constants'; + +const Container = styled.div` + min-height: ${LENS_VISUALIZATION_HEIGHT}px; +`; + +// when displaying chart in modal the tooltip is render under the modal +const LensChartTooltipFix = createGlobalStyle` + div.euiOverlayMask.euiOverlayMask--aboveHeader ~ [id^='echTooltipPortal'] { + z-index: ${({ theme }) => theme.eui.euiZLevel7} !important; + } +`; + +interface LensMarkDownRendererProps { + attributes: TypedLensByValueInput['attributes'] | null; + id?: string | null; + timeRange?: TypedLensByValueInput['timeRange']; + startDate?: string | null; + endDate?: string | null; + viewMode?: boolean | undefined; +} + +const LensMarkDownRendererComponent: React.FC = ({ + attributes, + timeRange, + viewMode = true, +}) => { + const location = useLocation(); + const { + application: { currentAppId$ }, + lens: { EmbeddableComponent, navigateToPrefilledEditor, canUseEditor }, + } = useKibana().services; + const [currentAppId, setCurrentAppId] = useState(undefined); + + const handleClick = useCallback(() => { + const options = viewMode + ? { + openInNewTab: true, + } + : { + originatingApp: currentAppId, + originatingPath: `${location.pathname}${location.search}`, + }; + + if (attributes) { + navigateToPrefilledEditor( + { + id: '', + timeRange, + attributes, + }, + options + ); + } + }, [ + attributes, + currentAppId, + location.pathname, + location.search, + navigateToPrefilledEditor, + timeRange, + viewMode, + ]); + + useEffect(() => { + const getCurrentAppId = async () => { + const appId = await currentAppId$.pipe(first()).toPromise(); + setCurrentAppId(appId); + }; + getCurrentAppId(); + }, [currentAppId$]); + + return ( + + {attributes ? ( + <> + + + +
{attributes.title}
+
+
+ + {viewMode && canUseEditor() ? ( + + {`Open visualization`} + + ) : null} + +
+ + + + + + + ) : null} +
+ ); +}; + +export const LensMarkDownRenderer = React.memo(LensMarkDownRendererComponent); diff --git a/x-pack/plugins/cases/public/components/markdown_editor/plugins/lens/translations.ts b/x-pack/plugins/cases/public/components/markdown_editor/plugins/lens/translations.ts new file mode 100644 index 0000000000000..8b09b88136054 --- /dev/null +++ b/x-pack/plugins/cases/public/components/markdown_editor/plugins/lens/translations.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; + +export const INSERT_LENS = i18n.translate( + 'xpack.cases.markdownEditor.plugins.lens.insertLensButtonLabel', + { + defaultMessage: 'Insert visualization', + } +); diff --git a/x-pack/plugins/cases/public/components/markdown_editor/plugins/lens/use_lens_draft_comment.ts b/x-pack/plugins/cases/public/components/markdown_editor/plugins/lens/use_lens_draft_comment.ts new file mode 100644 index 0000000000000..e615416b2a137 --- /dev/null +++ b/x-pack/plugins/cases/public/components/markdown_editor/plugins/lens/use_lens_draft_comment.ts @@ -0,0 +1,68 @@ +/* + * 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 { EuiMarkdownAstNodePosition } from '@elastic/eui'; +import { useCallback, useEffect, useState } from 'react'; +import { first } from 'rxjs/operators'; +import { useKibana } from '../../../../common/lib/kibana'; +import { DRAFT_COMMENT_STORAGE_ID } from './constants'; +import { INSERT_LENS } from './translations'; + +interface DraftComment { + commentId: string; + comment: string; + position: EuiMarkdownAstNodePosition; + title: string; +} + +export const useLensDraftComment = () => { + const { + application: { currentAppId$ }, + embeddable, + storage, + } = useKibana().services; + const [draftComment, setDraftComment] = useState(null); + + useEffect(() => { + const fetchDraftComment = async () => { + const currentAppId = await currentAppId$.pipe(first()).toPromise(); + + if (!currentAppId) { + return; + } + + const incomingEmbeddablePackage = embeddable + ?.getStateTransfer() + .getIncomingEmbeddablePackage(currentAppId); + + if (incomingEmbeddablePackage) { + if (storage.get(DRAFT_COMMENT_STORAGE_ID)) { + try { + setDraftComment(storage.get(DRAFT_COMMENT_STORAGE_ID)); + // eslint-disable-next-line no-empty + } catch (e) {} + } + } + }; + fetchDraftComment(); + }, [currentAppId$, embeddable, storage]); + + const openLensModal = useCallback(({ editorRef }) => { + if (editorRef && editorRef.textarea && editorRef.toolbar) { + const lensPluginButton = editorRef.toolbar?.querySelector(`[aria-label="${INSERT_LENS}"]`); + if (lensPluginButton) { + lensPluginButton.click(); + } + } + }, []); + + const clearDraftComment = useCallback(() => { + storage.remove(DRAFT_COMMENT_STORAGE_ID); + }, [storage]); + + return { draftComment, openLensModal, clearDraftComment }; +}; diff --git a/x-pack/plugins/cases/public/components/markdown_editor/types.ts b/x-pack/plugins/cases/public/components/markdown_editor/types.ts index ccc3c59c8977e..33249c0025f8e 100644 --- a/x-pack/plugins/cases/public/components/markdown_editor/types.ts +++ b/x-pack/plugins/cases/public/components/markdown_editor/types.ts @@ -22,7 +22,7 @@ export type TemporaryProcessingPluginsType = [ [ typeof rehype2react, Parameters[0] & { - components: { a: FunctionComponent; timeline: unknown }; + components: { a: FunctionComponent; lens: unknown; timeline: unknown }; } ], ...PluggableList diff --git a/x-pack/plugins/cases/public/components/markdown_editor/use_plugins.ts b/x-pack/plugins/cases/public/components/markdown_editor/use_plugins.ts index e98af8bca8bce..b87b9ae6ad09a 100644 --- a/x-pack/plugins/cases/public/components/markdown_editor/use_plugins.ts +++ b/x-pack/plugins/cases/public/components/markdown_editor/use_plugins.ts @@ -13,8 +13,11 @@ import { import { useMemo } from 'react'; import { useTimelineContext } from '../timeline_context/use_timeline_context'; import { TemporaryProcessingPluginsType } from './types'; +import { KibanaServices } from '../../common/lib/kibana'; +import * as lensMarkdownPlugin from './plugins/lens'; export const usePlugins = () => { + const kibanaConfig = KibanaServices.getConfig(); const timelinePlugins = useTimelineContext()?.editor_plugins; return useMemo(() => { @@ -31,10 +34,18 @@ export const usePlugins = () => { processingPlugins[1][1].components.timeline = timelinePlugins.processingPluginRenderer; } + if (kibanaConfig?.markdownPlugins?.lens) { + uiPlugins.push(lensMarkdownPlugin.plugin); + } + + parsingPlugins.push(lensMarkdownPlugin.parser); + // This line of code is TS-compatible and it will break if [1][1] change in the future. + processingPlugins[1][1].components.lens = lensMarkdownPlugin.renderer; + return { uiPlugins, parsingPlugins, processingPlugins, }; - }, [timelinePlugins]); + }, [kibanaConfig?.markdownPlugins?.lens, timelinePlugins]); }; diff --git a/x-pack/plugins/cases/public/components/user_action_tree/constants.ts b/x-pack/plugins/cases/public/components/user_action_tree/constants.ts new file mode 100644 index 0000000000000..584194be65f50 --- /dev/null +++ b/x-pack/plugins/cases/public/components/user_action_tree/constants.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 const DRAFT_COMMENT_STORAGE_ID = 'xpack.cases.commentDraft'; diff --git a/x-pack/plugins/cases/public/components/user_action_tree/index.tsx b/x-pack/plugins/cases/public/components/user_action_tree/index.tsx index 86247b503dff7..b7834585e7423 100644 --- a/x-pack/plugins/cases/public/components/user_action_tree/index.tsx +++ b/x-pack/plugins/cases/public/components/user_action_tree/index.tsx @@ -23,7 +23,7 @@ import * as i18n from './translations'; import { useUpdateComment } from '../../containers/use_update_comment'; import { useCurrentUser } from '../../common/lib/kibana'; -import { AddComment, AddCommentRefObject } from '../add_comment'; +import { AddComment } from '../add_comment'; import { ActionConnector, ActionsCommentRequestRt, @@ -55,6 +55,7 @@ import { UserActionTimestamp } from './user_action_timestamp'; import { UserActionUsername } from './user_action_username'; import { UserActionContentToolbar } from './user_action_content_toolbar'; import { getManualAlertIdsWithNoRuleId } from '../case_view/helpers'; +import { useLensDraftComment } from '../markdown_editor/plugins/lens/use_lens_draft_comment'; export interface UserActionTreeProps { caseServices: CaseServices; @@ -155,27 +156,25 @@ export const UserActionTree = React.memo( subCaseId?: string; }>(); const handlerTimeoutId = useRef(0); - const addCommentRef = useRef(null); const [initLoading, setInitLoading] = useState(true); const [selectedOutlineCommentId, setSelectedOutlineCommentId] = useState(''); const { isLoadingIds, patchComment } = useUpdateComment(); const currentUser = useCurrentUser(); - const [manageMarkdownEditIds, setManangeMardownEditIds] = useState([]); + const [manageMarkdownEditIds, setManageMarkdownEditIds] = useState([]); + const commentRefs = useRef>({}); + const { draftComment, openLensModal } = useLensDraftComment(); const [loadingAlertData, manualAlertsData] = useFetchAlertData( getManualAlertIdsWithNoRuleId(caseData.comments) ); - const handleManageMarkdownEditId = useCallback( - (id: string) => { - if (!manageMarkdownEditIds.includes(id)) { - setManangeMardownEditIds([...manageMarkdownEditIds, id]); - } else { - setManangeMardownEditIds(manageMarkdownEditIds.filter((myId) => id !== myId)); - } - }, - [manageMarkdownEditIds] - ); + const handleManageMarkdownEditId = useCallback((id: string) => { + setManageMarkdownEditIds((prevManageMarkdownEditIds) => + !prevManageMarkdownEditIds.includes(id) + ? prevManageMarkdownEditIds.concat(id) + : prevManageMarkdownEditIds.filter((myId) => id !== myId) + ); + }, []); const handleSaveComment = useCallback( ({ id, version }: { id: string; version: string }, content: string) => { @@ -220,8 +219,8 @@ export const UserActionTree = React.memo( (quote: string) => { const addCarrots = quote.replace(new RegExp('\r?\n', 'g'), ' \n> '); - if (addCommentRef && addCommentRef.current) { - addCommentRef.current.addQuote(`> ${addCarrots} \n`); + if (commentRefs.current[NEW_ID]) { + commentRefs.current[NEW_ID].addQuote(`> ${addCarrots} \n`); } handleOutlineComment('add-comment'); @@ -240,6 +239,7 @@ export const UserActionTree = React.memo( const MarkdownDescription = useMemo( () => ( (commentRefs.current[DESCRIPTION_ID] = element)} id={DESCRIPTION_ID} content={caseData.description} isEditable={manageMarkdownEditIds.includes(DESCRIPTION_ID)} @@ -255,9 +255,10 @@ export const UserActionTree = React.memo( const MarkdownNewComment = useMemo( () => ( (commentRefs.current[NEW_ID] = element)} onCommentPosted={handleUpdate} onCommentSaving={handleManageMarkdownEditId.bind(null, NEW_ID)} showLoading={false} @@ -357,6 +358,7 @@ export const UserActionTree = React.memo( }), children: ( (commentRefs.current[comment.id] = element)} id={comment.id} content={comment.comment} isEditable={manageMarkdownEditIds.includes(comment.id)} @@ -629,6 +631,30 @@ export const UserActionTree = React.memo( const comments = [...userActions, ...bottomActions]; + useEffect(() => { + if (draftComment?.commentId) { + setManageMarkdownEditIds((prevManageMarkdownEditIds) => { + if ( + ![NEW_ID].includes(draftComment?.commentId) && + !prevManageMarkdownEditIds.includes(draftComment?.commentId) + ) { + return [draftComment?.commentId]; + } + return prevManageMarkdownEditIds; + }); + + if ( + commentRefs.current && + commentRefs.current[draftComment.commentId] && + commentRefs.current[draftComment.commentId].editor?.textarea && + commentRefs.current[draftComment.commentId].editor?.toolbar + ) { + commentRefs.current[draftComment.commentId].setComment(draftComment.comment); + openLensModal({ editorRef: commentRefs.current[draftComment.commentId].editor }); + } + } + }, [draftComment, openLensModal]); + return ( <> diff --git a/x-pack/plugins/cases/public/components/user_action_tree/user_action_markdown.tsx b/x-pack/plugins/cases/public/components/user_action_tree/user_action_markdown.tsx index cf0d6e3ea50d1..f7a6932b35856 100644 --- a/x-pack/plugins/cases/public/components/user_action_tree/user_action_markdown.tsx +++ b/x-pack/plugins/cases/public/components/user_action_tree/user_action_markdown.tsx @@ -6,7 +6,7 @@ */ import { EuiFlexGroup, EuiFlexItem, EuiButtonEmpty, EuiButton } from '@elastic/eui'; -import React, { useCallback } from 'react'; +import React, { forwardRef, useCallback, useImperativeHandle, useMemo, useRef } from 'react'; import styled from 'styled-components'; import * as i18n from '../case_view/translations'; @@ -25,84 +25,96 @@ interface UserActionMarkdownProps { onChangeEditable: (id: string) => void; onSaveContent: (content: string) => void; } -export const UserActionMarkdown = ({ - id, - content, - isEditable, - onChangeEditable, - onSaveContent, -}: UserActionMarkdownProps) => { - const initialState = { content }; - const { form } = useForm({ - defaultValue: initialState, - options: { stripEmptyFields: false }, - schema, - }); - const fieldName = 'content'; - const { submit } = form; +interface UserActionMarkdownRefObject { + setComment: (newComment: string) => void; +} + +export const UserActionMarkdown = forwardRef( + ({ id, content, isEditable, onChangeEditable, onSaveContent }, ref) => { + const editorRef = useRef(); + const initialState = { content }; + const { form } = useForm({ + defaultValue: initialState, + options: { stripEmptyFields: false }, + schema, + }); + + const fieldName = 'content'; + const { setFieldValue, submit } = form; + + const handleCancelAction = useCallback(() => { + onChangeEditable(id); + }, [id, onChangeEditable]); + + const handleSaveAction = useCallback(async () => { + const { isValid, data } = await submit(); + if (isValid) { + onSaveContent(data.content); + } + onChangeEditable(id); + }, [id, onChangeEditable, onSaveContent, submit]); - const handleCancelAction = useCallback(() => { - onChangeEditable(id); - }, [id, onChangeEditable]); + const setComment = useCallback( + (newComment) => { + setFieldValue(fieldName, newComment); + }, + [setFieldValue] + ); - const handleSaveAction = useCallback(async () => { - const { isValid, data } = await submit(); - if (isValid) { - onSaveContent(data.content); - } - onChangeEditable(id); - }, [id, onChangeEditable, onSaveContent, submit]); + const EditorButtons = useMemo( + () => ( + + + + {i18n.CANCEL} + + + + + {i18n.SAVE} + + + + ), + [handleCancelAction, handleSaveAction] + ); - const renderButtons = useCallback( - ({ cancelAction, saveAction }) => ( - - - - {i18n.CANCEL} - - - - - {i18n.SAVE} - - - - ), - [] - ); + useImperativeHandle(ref, () => ({ + setComment, + editor: editorRef.current, + })); - return isEditable ? ( -
- - - ) : ( - - {content} - - ); -}; + return isEditable ? ( +
+ + + ) : ( + + {content} + + ); + } +); diff --git a/x-pack/plugins/cases/public/plugin.ts b/x-pack/plugins/cases/public/plugin.ts index 5bfdf9b8b9509..2b4fb40545548 100644 --- a/x-pack/plugins/cases/public/plugin.ts +++ b/x-pack/plugins/cases/public/plugin.ts @@ -17,7 +17,7 @@ import { getRecentCasesLazy, getAllCasesSelectorModalLazy, } from './methods'; -import { ENABLE_CASE_CONNECTOR } from '../common'; +import { CasesUiConfigType, ENABLE_CASE_CONNECTOR } from '../common'; /** * @public @@ -26,7 +26,7 @@ import { ENABLE_CASE_CONNECTOR } from '../common'; export class CasesUiPlugin implements Plugin { private kibanaVersion: string; - constructor(initializerContext: PluginInitializerContext) { + constructor(private readonly initializerContext: PluginInitializerContext) { this.kibanaVersion = initializerContext.env.packageInfo.version; } public setup(core: CoreSetup, plugins: SetupPlugins) { @@ -36,7 +36,8 @@ export class CasesUiPlugin implements Plugin(); + KibanaServices.init({ ...core, ...plugins, kibanaVersion: this.kibanaVersion, config }); return { /** * Get the all cases table diff --git a/x-pack/plugins/cases/public/types.ts b/x-pack/plugins/cases/public/types.ts index 2b31935c3ff97..db2e5d6ab6bff 100644 --- a/x-pack/plugins/cases/public/types.ts +++ b/x-pack/plugins/cases/public/types.ts @@ -7,11 +7,17 @@ import { CoreStart } from 'kibana/public'; import { ReactElement } from 'react'; + +import { LensPublicStart } from '../../lens/public'; import { SecurityPluginSetup } from '../../security/public'; -import { +import type { TriggersAndActionsUIPublicPluginSetup as TriggersActionsSetup, TriggersAndActionsUIPublicPluginStart as TriggersActionsStart, } from '../../triggers_actions_ui/public'; +import type { DataPublicPluginStart } from '../../../../src/plugins/data/public'; +import type { EmbeddableStart } from '../../../../src/plugins/embeddable/public'; +import type { Storage } from '../../../../src/plugins/kibana_utils/public'; + import { AllCasesProps } from './components/all_cases'; import { CaseViewProps } from './components/case_view'; import { ConfigureCasesProps } from './components/configure_cases'; @@ -25,6 +31,10 @@ export interface SetupPlugins { } export interface StartPlugins { + data: DataPublicPluginStart; + embeddable: EmbeddableStart; + lens: LensPublicStart; + storage: Storage; triggersActionsUi: TriggersActionsStart; } diff --git a/x-pack/plugins/cases/server/client/attachments/add.ts b/x-pack/plugins/cases/server/client/attachments/add.ts index dd1f09da5cb4a..166ae2ae65012 100644 --- a/x-pack/plugins/cases/server/client/attachments/add.ts +++ b/x-pack/plugins/cases/server/client/attachments/add.ts @@ -16,6 +16,7 @@ import { Logger, SavedObjectsUtils, } from '../../../../../../src/core/server'; +import { LensServerPluginSetup } from '../../../../lens/server'; import { nodeBuilder } from '../../../../../../src/plugins/data/common'; import { @@ -124,6 +125,7 @@ const addGeneratedAlerts = async ( caseService, userActionService, logger, + lensEmbeddableFactory, authorization, } = clientArgs; @@ -182,6 +184,7 @@ const addGeneratedAlerts = async ( unsecuredSavedObjectsClient, caseService, attachmentService, + lensEmbeddableFactory, }); const { @@ -241,12 +244,14 @@ async function getCombinedCase({ unsecuredSavedObjectsClient, id, logger, + lensEmbeddableFactory, }: { caseService: CasesService; attachmentService: AttachmentService; unsecuredSavedObjectsClient: SavedObjectsClientContract; id: string; logger: Logger; + lensEmbeddableFactory: LensServerPluginSetup['lensEmbeddableFactory']; }): Promise { const [casePromise, subCasePromise] = await Promise.allSettled([ caseService.getCase({ @@ -276,6 +281,7 @@ async function getCombinedCase({ caseService, attachmentService, unsecuredSavedObjectsClient, + lensEmbeddableFactory, }); } else { throw Boom.badRequest('Sub case found without reference to collection'); @@ -291,6 +297,7 @@ async function getCombinedCase({ caseService, attachmentService, unsecuredSavedObjectsClient, + lensEmbeddableFactory, }); } } @@ -332,6 +339,7 @@ export const addComment = async ( attachmentService, user, logger, + lensEmbeddableFactory, authorization, } = clientArgs; @@ -362,6 +370,7 @@ export const addComment = async ( unsecuredSavedObjectsClient, id: caseId, logger, + lensEmbeddableFactory, }); // eslint-disable-next-line @typescript-eslint/naming-convention diff --git a/x-pack/plugins/cases/server/client/attachments/update.ts b/x-pack/plugins/cases/server/client/attachments/update.ts index 157dd0b410898..da505ed55313c 100644 --- a/x-pack/plugins/cases/server/client/attachments/update.ts +++ b/x-pack/plugins/cases/server/client/attachments/update.ts @@ -9,6 +9,7 @@ import { pick } from 'lodash/fp'; import Boom from '@hapi/boom'; import { SavedObjectsClientContract, Logger } from 'kibana/server'; +import { LensServerPluginSetup } from '../../../../lens/server'; import { checkEnabledCaseConnectorOrThrow, CommentableCase, createCaseError } from '../../common'; import { buildCommentUserActionItem } from '../../services/user_actions/helpers'; import { @@ -46,6 +47,7 @@ interface CombinedCaseParams { unsecuredSavedObjectsClient: SavedObjectsClientContract; caseID: string; logger: Logger; + lensEmbeddableFactory: LensServerPluginSetup['lensEmbeddableFactory']; subCaseId?: string; } @@ -56,6 +58,7 @@ async function getCommentableCase({ caseID, subCaseId, logger, + lensEmbeddableFactory, }: CombinedCaseParams) { if (subCaseId) { const [caseInfo, subCase] = await Promise.all([ @@ -75,6 +78,7 @@ async function getCommentableCase({ subCase, unsecuredSavedObjectsClient, logger, + lensEmbeddableFactory, }); } else { const caseInfo = await caseService.getCase({ @@ -87,6 +91,7 @@ async function getCommentableCase({ collection: caseInfo, unsecuredSavedObjectsClient, logger, + lensEmbeddableFactory, }); } } @@ -105,6 +110,7 @@ export async function update( caseService, unsecuredSavedObjectsClient, logger, + lensEmbeddableFactory, user, userActionService, authorization, @@ -128,6 +134,7 @@ export async function update( caseID, subCaseId: subCaseID, logger, + lensEmbeddableFactory, }); const myComment = await attachmentService.get({ diff --git a/x-pack/plugins/cases/server/client/factory.ts b/x-pack/plugins/cases/server/client/factory.ts index 8fcfbe934c3ad..2fae6996f4aa2 100644 --- a/x-pack/plugins/cases/server/client/factory.ts +++ b/x-pack/plugins/cases/server/client/factory.ts @@ -25,6 +25,8 @@ import { } from '../services'; import { PluginStartContract as FeaturesPluginStart } from '../../../features/server'; import { PluginStartContract as ActionsPluginStart } from '../../../actions/server'; +import { LensServerPluginSetup } from '../../../lens/server'; + import { AuthorizationAuditLogger } from '../authorization'; import { CasesClient, createCasesClient } from '.'; @@ -34,6 +36,7 @@ interface CasesClientFactoryArgs { getSpace: GetSpaceFn; featuresPluginStart: FeaturesPluginStart; actionsPluginStart: ActionsPluginStart; + lensEmbeddableFactory: LensServerPluginSetup['lensEmbeddableFactory']; } /** @@ -108,6 +111,7 @@ export class CasesClientFactory { userActionService: new CaseUserActionService(this.logger), attachmentService: new AttachmentService(this.logger), logger: this.logger, + lensEmbeddableFactory: this.options.lensEmbeddableFactory, authorization: auth, actionsClient: await this.options.actionsPluginStart.getActionsClientWithRequest(request), }); diff --git a/x-pack/plugins/cases/server/client/types.ts b/x-pack/plugins/cases/server/client/types.ts index ebf79519da59a..27829d2539c7d 100644 --- a/x-pack/plugins/cases/server/client/types.ts +++ b/x-pack/plugins/cases/server/client/types.ts @@ -18,6 +18,7 @@ import { AttachmentService, } from '../services'; import { ActionsClient } from '../../../actions/server'; +import { LensServerPluginSetup } from '../../../lens/server'; /** * Parameters for initializing a cases client @@ -33,6 +34,7 @@ export interface CasesClientArgs { readonly alertsService: AlertServiceContract; readonly attachmentService: AttachmentService; readonly logger: Logger; + readonly lensEmbeddableFactory: LensServerPluginSetup['lensEmbeddableFactory']; readonly authorization: PublicMethodsOf; readonly actionsClient: PublicMethodsOf; } 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 03d6e5b8cea63..856d6378d5900 100644 --- a/x-pack/plugins/cases/server/common/models/commentable_case.ts +++ b/x-pack/plugins/cases/server/common/models/commentable_case.ts @@ -10,9 +10,11 @@ import { SavedObject, SavedObjectReference, SavedObjectsClientContract, + SavedObjectsUpdateOptions, SavedObjectsUpdateResponse, Logger, } from 'src/core/server'; +import { LensServerPluginSetup } from '../../../../lens/server'; import { AssociationType, CASE_SAVED_OBJECT, @@ -29,12 +31,14 @@ import { SUB_CASE_SAVED_OBJECT, SubCaseAttributes, User, + CommentRequestUserType, CaseAttributes, } from '../../../common'; import { flattenCommentSavedObjects, flattenSubCaseSavedObject, transformNewComment } from '..'; import { AttachmentService, CasesService } from '../../services'; import { createCaseError } from '../error'; import { countAlertsForID } from '../index'; +import { getOrUpdateLensReferences } from '../utils'; interface UpdateCommentResp { comment: SavedObjectsUpdateResponse; @@ -53,6 +57,7 @@ interface CommentableCaseParams { caseService: CasesService; attachmentService: AttachmentService; logger: Logger; + lensEmbeddableFactory: LensServerPluginSetup['lensEmbeddableFactory']; } /** @@ -66,6 +71,7 @@ export class CommentableCase { private readonly caseService: CasesService; private readonly attachmentService: AttachmentService; private readonly logger: Logger; + private readonly lensEmbeddableFactory: LensServerPluginSetup['lensEmbeddableFactory']; constructor({ collection, @@ -74,6 +80,7 @@ export class CommentableCase { caseService, attachmentService, logger, + lensEmbeddableFactory, }: CommentableCaseParams) { this.collection = collection; this.subCase = subCase; @@ -81,6 +88,7 @@ export class CommentableCase { this.caseService = caseService; this.attachmentService = attachmentService; this.logger = logger; + this.lensEmbeddableFactory = lensEmbeddableFactory; } public get status(): CaseStatuses { @@ -188,6 +196,7 @@ export class CommentableCase { caseService: this.caseService, attachmentService: this.attachmentService, logger: this.logger, + lensEmbeddableFactory: this.lensEmbeddableFactory, }); } catch (error) { throw createCaseError({ @@ -212,6 +221,23 @@ export class CommentableCase { }): Promise { try { const { id, version, ...queryRestAttributes } = updateRequest; + const options: SavedObjectsUpdateOptions = { + version, + }; + + if (queryRestAttributes.type === CommentType.user && queryRestAttributes?.comment) { + const currentComment = (await this.attachmentService.get({ + unsecuredSavedObjectsClient: this.unsecuredSavedObjectsClient, + attachmentId: id, + })) as SavedObject; + + const updatedReferences = getOrUpdateLensReferences( + this.lensEmbeddableFactory, + queryRestAttributes.comment, + currentComment + ); + options.references = updatedReferences; + } const [comment, commentableCase] = await Promise.all([ this.attachmentService.update({ @@ -222,7 +248,7 @@ export class CommentableCase { updated_at: updatedAt, updated_by: user, }, - version, + options, }), this.update({ date: updatedAt, user }), ]); @@ -268,6 +294,16 @@ export class CommentableCase { throw Boom.badRequest('The owner field of the comment must match the case'); } + let references = this.buildRefsToCase(); + + if (commentReq.type === CommentType.user && commentReq?.comment) { + const commentStringReferences = getOrUpdateLensReferences( + this.lensEmbeddableFactory, + commentReq.comment + ); + references = [...references, ...commentStringReferences]; + } + const [comment, commentableCase] = await Promise.all([ this.attachmentService.create({ unsecuredSavedObjectsClient: this.unsecuredSavedObjectsClient, @@ -277,7 +313,7 @@ export class CommentableCase { ...commentReq, ...user, }), - references: this.buildRefsToCase(), + references, id, }), this.update({ date: createdDate, user }), diff --git a/x-pack/plugins/cases/server/common/utils.test.ts b/x-pack/plugins/cases/server/common/utils.test.ts index 46ba33a74acd6..e45b91a28ceb3 100644 --- a/x-pack/plugins/cases/server/common/utils.test.ts +++ b/x-pack/plugins/cases/server/common/utils.test.ts @@ -5,13 +5,15 @@ * 2.0. */ -import { SavedObjectsFindResponse } from 'kibana/server'; +import { SavedObject, SavedObjectsFindResponse } from 'kibana/server'; +import { lensEmbeddableFactory } from '../../../lens/server/embeddable/lens_embeddable_factory'; import { SECURITY_SOLUTION_OWNER } from '../../common'; import { AssociationType, CaseResponse, CommentAttributes, CommentRequest, + CommentRequestUserType, CommentType, } from '../../common/api'; import { mockCaseComments, mockCases } from '../routes/api/__fixtures__/mock_saved_objects'; @@ -25,6 +27,8 @@ import { transformComments, flattenCommentSavedObjects, flattenCommentSavedObject, + extractLensReferencesFromCommentString, + getOrUpdateLensReferences, } from './utils'; interface CommentReference { @@ -865,4 +869,130 @@ describe('common utils', () => { ).toEqual(2); }); }); + + describe('extractLensReferencesFromCommentString', () => { + it('extracts successfully', () => { + const commentString = [ + '**Test** ', + 'Amazingg!!!', + '[asdasdasdasd](http://localhost:5601/moq/app/security/timelines?timeline=(id%3A%27e4362a60-f478-11eb-a4b0-ebefce184d8d%27%2CisOpen%3A!t))', + '!{lens{"timeRange":{"from":"now-7d","to":"now","mode":"relative"},"attributes":{"title":"aaaa","type":"lens","visualizationType":"lnsXY","state":{"datasourceStates":{"indexpattern":{"layers":{"layer1":{"columnOrder":["col1","col2"],"columns":{"col2":{"dataType":"number","isBucketed":false,"label":"Count of records","operationType":"count","scale":"ratio","sourceField":"Records"},"col1":{"dataType":"date","isBucketed":true,"label":"@timestamp","operationType":"date_histogram","params":{"interval":"auto"},"scale":"interval","sourceField":"timestamp"}}}}}},"visualization":{"axisTitlesVisibilitySettings":{"x":true,"yLeft":true,"yRight":true},"fittingFunction":"None","gridlinesVisibilitySettings":{"x":true,"yLeft":true,"yRight":true},"layers":[{"accessors":["col2"],"layerId":"layer1","seriesType":"bar_stacked","xAccessor":"col1","yConfig":[{"forAccessor":"col2"}]}],"legend":{"isVisible":true,"position":"right"},"preferredSeriesType":"bar_stacked","tickLabelsVisibilitySettings":{"x":true,"yLeft":true,"yRight":true},"valueLabels":"hide","yRightExtent":{"mode":"full"}},"query":{"language":"kuery","query":""},"filters":[]},"references":[{"type":"index-pattern","id":"90943e30-9a47-11e8-b64d-95841ca0b246","name":"indexpattern-datasource-current-indexpattern"},{"type":"index-pattern","id":"90943e30-9a47-11e8-b64d-95841ca0b248","name":"indexpattern-datasource-layer-layer1"}]},"editMode":false}}', + '!{lens{"timeRange":{"from":"now-7d","to":"now","mode":"relative"},"attributes":{"title":"aaaa","type":"lens","visualizationType":"lnsXY","state":{"datasourceStates":{"indexpattern":{"layers":{"layer1":{"columnOrder":["col1","col2"],"columns":{"col2":{"dataType":"number","isBucketed":false,"label":"Count of records","operationType":"count","scale":"ratio","sourceField":"Records"},"col1":{"dataType":"date","isBucketed":true,"label":"@timestamp","operationType":"date_histogram","params":{"interval":"auto"},"scale":"interval","sourceField":"timestamp"}}}}}},"visualization":{"axisTitlesVisibilitySettings":{"x":true,"yLeft":true,"yRight":true},"fittingFunction":"None","gridlinesVisibilitySettings":{"x":true,"yLeft":true,"yRight":true},"layers":[{"accessors":["col2"],"layerId":"layer1","seriesType":"bar_stacked","xAccessor":"col1","yConfig":[{"forAccessor":"col2"}]}],"legend":{"isVisible":true,"position":"right"},"preferredSeriesType":"bar_stacked","tickLabelsVisibilitySettings":{"x":true,"yLeft":true,"yRight":true},"valueLabels":"hide","yRightExtent":{"mode":"full"}},"query":{"language":"kuery","query":""},"filters":[]},"references":[{"type":"index-pattern","id":"90943e30-9a47-11e8-b64d-95841ca0b246","name":"indexpattern-datasource-current-indexpattern"},{"type":"index-pattern","id":"90943e30-9a47-11e8-b64d-95841ca0b247","name":"indexpattern-datasource-layer-layer1"}]},"editMode":false}}', + ].join('\n\n'); + + const extractedReferences = extractLensReferencesFromCommentString( + lensEmbeddableFactory, + commentString + ); + + const expectedReferences = [ + { + type: 'index-pattern', + id: '90943e30-9a47-11e8-b64d-95841ca0b246', + name: 'indexpattern-datasource-current-indexpattern', + }, + { + type: 'index-pattern', + id: '90943e30-9a47-11e8-b64d-95841ca0b248', + name: 'indexpattern-datasource-layer-layer1', + }, + { + type: 'index-pattern', + id: '90943e30-9a47-11e8-b64d-95841ca0b247', + name: 'indexpattern-datasource-layer-layer1', + }, + ]; + + expect(expectedReferences.length).toEqual(extractedReferences.length); + expect(expectedReferences).toEqual(expect.arrayContaining(extractedReferences)); + }); + }); + + describe('getOrUpdateLensReferences', () => { + it('update references', () => { + const currentCommentStringReferences = [ + [ + { + type: 'index-pattern', + id: '90943e30-9a47-11e8-b64d-95841ca0b246', + name: 'indexpattern-datasource-current-indexpattern', + }, + { + type: 'index-pattern', + id: '90943e30-9a47-11e8-b64d-95841ca0b248', + name: 'indexpattern-datasource-layer-layer1', + }, + ], + [ + { + type: 'index-pattern', + id: '90943e30-9a47-11e8-b64d-95841ca0b246', + name: 'indexpattern-datasource-current-indexpattern', + }, + { + type: 'index-pattern', + id: '90943e30-9a47-11e8-b64d-95841ca0b248', + name: 'indexpattern-datasource-layer-layer1', + }, + ], + ]; + const currentCommentString = [ + '**Test** ', + '[asdasdasdasd](http://localhost:5601/moq/app/security/timelines?timeline=(id%3A%27e4362a60-f478-11eb-a4b0-ebefce184d8d%27%2CisOpen%3A!t))', + `!{lens{"timeRange":{"from":"now-7d","to":"now","mode":"relative"},"attributes":{"title":"aaaa","type":"lens","visualizationType":"lnsXY","state":{"datasourceStates":{"indexpattern":{"layers":{"layer1":{"columnOrder":["col1","col2"],"columns":{"col2":{"dataType":"number","isBucketed":false,"label":"Count of records","operationType":"count","scale":"ratio","sourceField":"Records"},"col1":{"dataType":"date","isBucketed":true,"label":"@timestamp","operationType":"date_histogram","params":{"interval":"auto"},"scale":"interval","sourceField":"timestamp"}}}}}},"visualization":{"axisTitlesVisibilitySettings":{"x":true,"yLeft":true,"yRight":true},"fittingFunction":"None","gridlinesVisibilitySettings":{"x":true,"yLeft":true,"yRight":true},"layers":[{"accessors":["col2"],"layerId":"layer1","seriesType":"bar_stacked","xAccessor":"col1","yConfig":[{"forAccessor":"col2"}]}],"legend":{"isVisible":true,"position":"right"},"preferredSeriesType":"bar_stacked","tickLabelsVisibilitySettings":{"x":true,"yLeft":true,"yRight":true},"valueLabels":"hide","yRightExtent":{"mode":"full"}},"query":{"language":"kuery","query":""},"filters":[]},"references":${JSON.stringify( + currentCommentStringReferences[0] + )}},"editMode":false}}`, + `!{lens{"timeRange":{"from":"now-7d","to":"now","mode":"relative"},"attributes":{"title":"aaaa","type":"lens","visualizationType":"lnsXY","state":{"datasourceStates":{"indexpattern":{"layers":{"layer1":{"columnOrder":["col1","col2"],"columns":{"col2":{"dataType":"number","isBucketed":false,"label":"Count of records","operationType":"count","scale":"ratio","sourceField":"Records"},"col1":{"dataType":"date","isBucketed":true,"label":"@timestamp","operationType":"date_histogram","params":{"interval":"auto"},"scale":"interval","sourceField":"timestamp"}}}}}},"visualization":{"axisTitlesVisibilitySettings":{"x":true,"yLeft":true,"yRight":true},"fittingFunction":"None","gridlinesVisibilitySettings":{"x":true,"yLeft":true,"yRight":true},"layers":[{"accessors":["col2"],"layerId":"layer1","seriesType":"bar_stacked","xAccessor":"col1","yConfig":[{"forAccessor":"col2"}]}],"legend":{"isVisible":true,"position":"right"},"preferredSeriesType":"bar_stacked","tickLabelsVisibilitySettings":{"x":true,"yLeft":true,"yRight":true},"valueLabels":"hide","yRightExtent":{"mode":"full"}},"query":{"language":"kuery","query":""},"filters":[]},"references":${JSON.stringify( + currentCommentStringReferences[1] + )}},"editMode":false}}`, + ].join('\n\n'); + const nonLensCurrentCommentReferences = [ + { type: 'case', id: '7b4be181-9646-41b8-b12d-faabf1bd9512', name: 'Test case' }, + { + type: 'timeline', + id: '0f847d31-9683-4ebd-92b9-454e3e39aec1', + name: 'Test case timeline', + }, + ]; + const currentCommentReferences = [ + ...currentCommentStringReferences.flat(), + ...nonLensCurrentCommentReferences, + ]; + const newCommentStringReferences = [ + { + type: 'index-pattern', + id: '90943e30-9a47-11e8-b64d-95841ca0b245', + name: 'indexpattern-datasource-current-indexpattern', + }, + { + type: 'index-pattern', + id: '90943e30-9a47-11e8-b64d-95841ca0b248', + name: 'indexpattern-datasource-layer-layer1', + }, + ]; + const newCommentString = [ + '**Test** ', + 'Awmazingg!!!', + '[asdasdasdasd](http://localhost:5601/moq/app/security/timelines?timeline=(id%3A%27e4362a60-f478-11eb-a4b0-ebefce184d8d%27%2CisOpen%3A!t))', + `!{lens{"timeRange":{"from":"now-7d","to":"now","mode":"relative"},"attributes":{"title":"aaaa","type":"lens","visualizationType":"lnsXY","state":{"datasourceStates":{"indexpattern":{"layers":{"layer1":{"columnOrder":["col1","col2"],"columns":{"col2":{"dataType":"number","isBucketed":false,"label":"Count of records","operationType":"count","scale":"ratio","sourceField":"Records"},"col1":{"dataType":"date","isBucketed":true,"label":"@timestamp","operationType":"date_histogram","params":{"interval":"auto"},"scale":"interval","sourceField":"timestamp"}}}}}},"visualization":{"axisTitlesVisibilitySettings":{"x":true,"yLeft":true,"yRight":true},"fittingFunction":"None","gridlinesVisibilitySettings":{"x":true,"yLeft":true,"yRight":true},"layers":[{"accessors":["col2"],"layerId":"layer1","seriesType":"bar_stacked","xAccessor":"col1","yConfig":[{"forAccessor":"col2"}]}],"legend":{"isVisible":true,"position":"right"},"preferredSeriesType":"bar_stacked","tickLabelsVisibilitySettings":{"x":true,"yLeft":true,"yRight":true},"valueLabels":"hide","yRightExtent":{"mode":"full"}},"query":{"language":"kuery","query":""},"filters":[]},"references":${JSON.stringify( + newCommentStringReferences + )}},"editMode":false}}`, + ].join('\n\n'); + + const updatedReferences = getOrUpdateLensReferences(lensEmbeddableFactory, newCommentString, { + references: currentCommentReferences, + attributes: { + comment: currentCommentString, + }, + } as SavedObject); + + const expectedReferences = [ + ...nonLensCurrentCommentReferences, + ...newCommentStringReferences, + ]; + + expect(expectedReferences.length).toEqual(updatedReferences.length); + expect(expectedReferences).toEqual(expect.arrayContaining(updatedReferences)); + }); + }); }); diff --git a/x-pack/plugins/cases/server/common/utils.ts b/x-pack/plugins/cases/server/common/utils.ts index bce37764467df..ba7d56f51eea9 100644 --- a/x-pack/plugins/cases/server/common/utils.ts +++ b/x-pack/plugins/cases/server/common/utils.ts @@ -4,11 +4,26 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ + import Boom from '@hapi/boom'; +import unified from 'unified'; +import type { Node, Parent } from 'unist'; +// installed by @elastic/eui +// eslint-disable-next-line import/no-extraneous-dependencies +import markdown from 'remark-parse'; +import remarkStringify from 'remark-stringify'; -import { SavedObjectsFindResult, SavedObjectsFindResponse, SavedObject } from 'kibana/server'; -import { isEmpty } from 'lodash'; +import { + SavedObjectsFindResult, + SavedObjectsFindResponse, + SavedObject, + SavedObjectReference, +} from 'kibana/server'; +import { filter, flatMap, uniqWith, isEmpty, xorWith } from 'lodash'; +import { TimeRange } from 'src/plugins/data/server'; +import { EmbeddableStateWithType } from 'src/plugins/embeddable/common'; import { AlertInfo } from '.'; +import { LensServerPluginSetup, LensDocShape715 } from '../../../lens/server'; import { AssociationType, @@ -33,6 +48,8 @@ import { User, } from '../../common'; import { UpdateAlertRequest } from '../client/alerts/types'; +import { LENS_ID, LensParser, LensSerializer } from '../../common/utils/markdown_plugins/lens'; +import { TimelineSerializer, TimelineParser } from '../../common/utils/markdown_plugins/timeline'; /** * Default sort field for querying saved objects. @@ -398,3 +415,89 @@ export const getNoneCaseConnector = () => ({ type: ConnectorTypes.none, fields: null, }); + +interface LensMarkdownNode extends EmbeddableStateWithType { + timeRange: TimeRange; + attributes: LensDocShape715 & { references: SavedObjectReference[] }; +} + +export const parseCommentString = (comment: string) => { + const processor = unified().use([[markdown, {}], LensParser, TimelineParser]); + return processor.parse(comment) as Parent; +}; + +export const stringifyComment = (comment: Parent) => + unified() + .use([ + [ + remarkStringify, + { + allowDangerousHtml: true, + handlers: { + /* + because we're using rison in the timeline url we need + to make sure that markdown parser doesn't modify the url + */ + timeline: TimelineSerializer, + lens: LensSerializer, + }, + }, + ], + ]) + .stringify(comment); + +export const getLensVisualizations = (parsedComment: Array) => + filter(parsedComment, { type: LENS_ID }) as LensMarkdownNode[]; + +export const extractLensReferencesFromCommentString = ( + lensEmbeddableFactory: LensServerPluginSetup['lensEmbeddableFactory'], + comment: string +): SavedObjectReference[] => { + const extract = lensEmbeddableFactory()?.extract; + + if (extract) { + const parsedComment = parseCommentString(comment); + const lensVisualizations = getLensVisualizations(parsedComment.children); + const flattenRefs = flatMap( + lensVisualizations, + (lensObject) => extract(lensObject)?.references ?? [] + ); + + const uniqRefs = uniqWith( + flattenRefs, + (refA, refB) => refA.type === refB.type && refA.id === refB.id && refA.name === refB.name + ); + + return uniqRefs; + } + return []; +}; + +export const getOrUpdateLensReferences = ( + lensEmbeddableFactory: LensServerPluginSetup['lensEmbeddableFactory'], + newComment: string, + currentComment?: SavedObject +) => { + if (!currentComment) { + return extractLensReferencesFromCommentString(lensEmbeddableFactory, newComment); + } + + const savedObjectReferences = currentComment.references; + const savedObjectLensReferences = extractLensReferencesFromCommentString( + lensEmbeddableFactory, + currentComment.attributes.comment + ); + + const currentNonLensReferences = xorWith( + savedObjectReferences, + savedObjectLensReferences, + (refA, refB) => refA.type === refB.type && refA.id === refB.id + ); + + const newCommentLensReferences = extractLensReferencesFromCommentString( + lensEmbeddableFactory, + newComment + ); + + return currentNonLensReferences.concat(newCommentLensReferences); +}; diff --git a/x-pack/plugins/cases/server/config.ts b/x-pack/plugins/cases/server/config.ts index 7679a5a389051..317f15283e112 100644 --- a/x-pack/plugins/cases/server/config.ts +++ b/x-pack/plugins/cases/server/config.ts @@ -9,6 +9,9 @@ import { schema, TypeOf } from '@kbn/config-schema'; export const ConfigSchema = schema.object({ enabled: schema.boolean({ defaultValue: true }), + markdownPlugins: schema.object({ + lens: schema.boolean({ defaultValue: false }), + }), }); export type ConfigType = TypeOf; diff --git a/x-pack/plugins/cases/server/index.ts b/x-pack/plugins/cases/server/index.ts index 4526ecce28460..5e433b46b80e5 100644 --- a/x-pack/plugins/cases/server/index.ts +++ b/x-pack/plugins/cases/server/index.ts @@ -12,6 +12,9 @@ import { CasePlugin } from './plugin'; export const config: PluginConfigDescriptor = { schema: ConfigSchema, + exposeToBrowser: { + markdownPlugins: true, + }, deprecations: ({ renameFromRoot }) => [ renameFromRoot('xpack.case.enabled', 'xpack.cases.enabled'), ], diff --git a/x-pack/plugins/cases/server/plugin.ts b/x-pack/plugins/cases/server/plugin.ts index b1e2f61a595ee..bb1be163585a8 100644 --- a/x-pack/plugins/cases/server/plugin.ts +++ b/x-pack/plugins/cases/server/plugin.ts @@ -18,7 +18,7 @@ import { APP_ID, ENABLE_CASE_CONNECTOR } from '../common'; import { ConfigType } from './config'; import { initCaseApi } from './routes/api'; import { - caseCommentSavedObjectType, + createCaseCommentSavedObjectType, caseConfigureSavedObjectType, caseConnectorMappingsSavedObjectType, caseSavedObjectType, @@ -32,6 +32,7 @@ import type { CasesRequestHandlerContext } from './types'; import { CasesClientFactory } from './client/factory'; import { SpacesPluginStart } from '../../spaces/server'; import { PluginStartContract as FeaturesPluginStart } from '../../features/server'; +import { LensServerPluginSetup } from '../../lens/server'; function createConfig(context: PluginInitializerContext) { return context.config.get(); @@ -40,6 +41,7 @@ function createConfig(context: PluginInitializerContext) { export interface PluginsSetup { security?: SecurityPluginSetup; actions: ActionsPluginSetup; + lens: LensServerPluginSetup; } export interface PluginsStart { @@ -66,6 +68,7 @@ export class CasePlugin { private readonly log: Logger; private clientFactory: CasesClientFactory; private securityPluginSetup?: SecurityPluginSetup; + private lensEmbeddableFactory?: LensServerPluginSetup['lensEmbeddableFactory']; constructor(private readonly initializerContext: PluginInitializerContext) { this.log = this.initializerContext.logger.get(); @@ -80,8 +83,15 @@ export class CasePlugin { } this.securityPluginSetup = plugins.security; + this.lensEmbeddableFactory = plugins.lens.lensEmbeddableFactory; - core.savedObjects.registerType(caseCommentSavedObjectType); + core.savedObjects.registerType( + createCaseCommentSavedObjectType({ + migrationDeps: { + lensEmbeddableFactory: this.lensEmbeddableFactory, + }, + }) + ); core.savedObjects.registerType(caseConfigureSavedObjectType); core.savedObjects.registerType(caseConnectorMappingsSavedObjectType); core.savedObjects.registerType(caseSavedObjectType); @@ -127,6 +137,7 @@ export class CasePlugin { }, featuresPluginStart: plugins.features, actionsPluginStart: plugins.actions, + lensEmbeddableFactory: this.lensEmbeddableFactory!, }); const client = core.elasticsearch.client; diff --git a/x-pack/plugins/cases/server/saved_object_types/comments.ts b/x-pack/plugins/cases/server/saved_object_types/comments.ts index 876ceb9bc2045..0384a65dcb389 100644 --- a/x-pack/plugins/cases/server/saved_object_types/comments.ts +++ b/x-pack/plugins/cases/server/saved_object_types/comments.ts @@ -7,11 +7,15 @@ import { SavedObjectsType } from 'src/core/server'; import { CASE_COMMENT_SAVED_OBJECT } from '../../common'; -import { commentsMigrations } from './migrations'; +import { createCommentsMigrations, CreateCommentsMigrationsDeps } from './migrations'; -export const caseCommentSavedObjectType: SavedObjectsType = { +export const createCaseCommentSavedObjectType = ({ + migrationDeps, +}: { + migrationDeps: CreateCommentsMigrationsDeps; +}): SavedObjectsType => ({ name: CASE_COMMENT_SAVED_OBJECT, - hidden: true, + hidden: false, namespaceType: 'single', mappings: { properties: { @@ -105,5 +109,5 @@ export const caseCommentSavedObjectType: SavedObjectsType = { }, }, }, - migrations: commentsMigrations, -}; + migrations: () => createCommentsMigrations(migrationDeps), +}); diff --git a/x-pack/plugins/cases/server/saved_object_types/index.ts b/x-pack/plugins/cases/server/saved_object_types/index.ts index 1c6bcf6ca710a..2c39a10f61da7 100644 --- a/x-pack/plugins/cases/server/saved_object_types/index.ts +++ b/x-pack/plugins/cases/server/saved_object_types/index.ts @@ -8,6 +8,6 @@ export { caseSavedObjectType } from './cases'; export { subCaseSavedObjectType } from './sub_case'; export { caseConfigureSavedObjectType } from './configure'; -export { caseCommentSavedObjectType } from './comments'; +export { createCaseCommentSavedObjectType } from './comments'; export { caseUserActionSavedObjectType } from './user_actions'; export { caseConnectorMappingsSavedObjectType } from './connector_mappings'; diff --git a/x-pack/plugins/cases/server/saved_object_types/migrations/index.test.ts b/x-pack/plugins/cases/server/saved_object_types/migrations/index.test.ts new file mode 100644 index 0000000000000..595ecf290c520 --- /dev/null +++ b/x-pack/plugins/cases/server/saved_object_types/migrations/index.test.ts @@ -0,0 +1,236 @@ +/* + * 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 { createCommentsMigrations } from './index'; +import { getLensVisualizations, parseCommentString } from '../../common'; + +import { savedObjectsServiceMock } from '../../../../../../src/core/server/mocks'; +import { lensEmbeddableFactory } from '../../../../lens/server/embeddable/lens_embeddable_factory'; + +const migrations = createCommentsMigrations({ + lensEmbeddableFactory, +}); + +const contextMock = savedObjectsServiceMock.createMigrationContext(); + +describe('lens embeddable migrations for by value panels', () => { + describe('7.14.0 remove time zone from Lens visualization date histogram', () => { + const lensVisualizationToMigrate = { + title: 'MyRenamedOps', + description: '', + visualizationType: 'lnsXY', + state: { + datasourceStates: { + indexpattern: { + layers: { + '2': { + columns: { + '3': { + label: '@timestamp', + dataType: 'date', + operationType: 'date_histogram', + sourceField: '@timestamp', + isBucketed: true, + scale: 'interval', + params: { interval: 'auto', timeZone: 'Europe/Berlin' }, + }, + '4': { + label: '@timestamp', + dataType: 'date', + operationType: 'date_histogram', + sourceField: '@timestamp', + isBucketed: true, + scale: 'interval', + params: { interval: 'auto' }, + }, + '5': { + label: '@timestamp', + dataType: 'date', + operationType: 'my_unexpected_operation', + isBucketed: true, + scale: 'interval', + params: { timeZone: 'do not delete' }, + }, + }, + columnOrder: ['3', '4', '5'], + incompleteColumns: {}, + }, + }, + }, + }, + visualization: { + title: 'Empty XY chart', + legend: { isVisible: true, position: 'right' }, + valueLabels: 'hide', + preferredSeriesType: 'bar_stacked', + layers: [ + { + layerId: '5ab74ddc-93ca-44e2-9857-ecf85c86b53e', + accessors: [ + '5fea2a56-7b73-44b5-9a50-7f0c0c4f8fd0', + 'e5efca70-edb5-4d6d-a30a-79384066987e', + '7ffb7bde-4f42-47ab-b74d-1b4fd8393e0f', + ], + position: 'top', + seriesType: 'bar_stacked', + showGridlines: false, + xAccessor: '2e57a41e-5a52-42d3-877f-bd211d903ef8', + }, + ], + }, + query: { query: '', language: 'kuery' }, + filters: [], + }, + }; + + const expectedLensVisualizationMigrated = { + title: 'MyRenamedOps', + description: '', + visualizationType: 'lnsXY', + state: { + datasourceStates: { + indexpattern: { + layers: { + '2': { + columns: { + '3': { + label: '@timestamp', + dataType: 'date', + operationType: 'date_histogram', + sourceField: '@timestamp', + isBucketed: true, + scale: 'interval', + params: { interval: 'auto' }, + }, + '4': { + label: '@timestamp', + dataType: 'date', + operationType: 'date_histogram', + sourceField: '@timestamp', + isBucketed: true, + scale: 'interval', + params: { interval: 'auto' }, + }, + '5': { + label: '@timestamp', + dataType: 'date', + operationType: 'my_unexpected_operation', + isBucketed: true, + scale: 'interval', + params: { timeZone: 'do not delete' }, + }, + }, + columnOrder: ['3', '4', '5'], + incompleteColumns: {}, + }, + }, + }, + }, + visualization: { + title: 'Empty XY chart', + legend: { isVisible: true, position: 'right' }, + valueLabels: 'hide', + preferredSeriesType: 'bar_stacked', + layers: [ + { + layerId: '5ab74ddc-93ca-44e2-9857-ecf85c86b53e', + accessors: [ + '5fea2a56-7b73-44b5-9a50-7f0c0c4f8fd0', + 'e5efca70-edb5-4d6d-a30a-79384066987e', + '7ffb7bde-4f42-47ab-b74d-1b4fd8393e0f', + ], + position: 'top', + seriesType: 'bar_stacked', + showGridlines: false, + xAccessor: '2e57a41e-5a52-42d3-877f-bd211d903ef8', + }, + ], + }, + query: { query: '', language: 'kuery' }, + filters: [], + }, + }; + + const expectedMigrationCommentResult = `"**Amazing**\n\n!{tooltip[Tessss](https://example.com)}\n\nbrbrbr\n\n[asdasdasdasd](http://localhost:5601/moq/app/security/timelines?timeline=(id%3A%27e4362a60-f478-11eb-a4b0-ebefce184d8d%27%2CisOpen%3A!t))\n\n!{lens{\"timeRange\":{\"from\":\"now-7d\",\"to\":\"now\",\"mode\":\"relative\"},\"attributes\":${JSON.stringify( + expectedLensVisualizationMigrated + )}}}\n\n!{lens{\"timeRange\":{\"from\":\"now-7d\",\"to\":\"now\",\"mode\":\"relative\"},\"attributes\":{\"title\":\"TEst22\",\"type\":\"lens\",\"visualizationType\":\"lnsMetric\",\"state\":{\"datasourceStates\":{\"indexpattern\":{\"layers\":{\"layer1\":{\"columnOrder\":[\"col2\"],\"columns\":{\"col2\":{\"dataType\":\"number\",\"isBucketed\":false,\"label\":\"Count of records\",\"operationType\":\"count\",\"scale\":\"ratio\",\"sourceField\":\"Records\"}}}}}},\"visualization\":{\"layerId\":\"layer1\",\"accessor\":\"col2\"},\"query\":{\"language\":\"kuery\",\"query\":\"\"},\"filters\":[]},\"references\":[{\"type\":\"index-pattern\",\"id\":\"90943e30-9a47-11e8-b64d-95841ca0b247\",\"name\":\"indexpattern-datasource-current-indexpattern\"},{\"type\":\"index-pattern\",\"id\":\"90943e30-9a47-11e8-b64d-95841ca0b247\",\"name\":\"indexpattern-datasource-layer-layer1\"}]}}}\n\nbrbrbr" +`; + + const caseComment = { + type: 'cases-comments', + id: '1cefd0d0-e86d-11eb-bae5-3d065cd16a32', + attributes: { + associationType: 'case', + comment: `"**Amazing**\n\n!{tooltip[Tessss](https://example.com)}\n\nbrbrbr\n\n[asdasdasdasd](http://localhost:5601/moq/app/security/timelines?timeline=(id%3A%27e4362a60-f478-11eb-a4b0-ebefce184d8d%27%2CisOpen%3A!t))\n\n!{lens{\"timeRange\":{\"from\":\"now-7d\",\"to\":\"now\",\"mode\":\"relative\"},\"editMode\":false,\"attributes\":${JSON.stringify( + lensVisualizationToMigrate + )}}}\n\n!{lens{\"timeRange\":{\"from\":\"now-7d\",\"to\":\"now\",\"mode\":\"relative\"},\"editMode\":false,\"attributes\":{\"title\":\"TEst22\",\"type\":\"lens\",\"visualizationType\":\"lnsMetric\",\"state\":{\"datasourceStates\":{\"indexpattern\":{\"layers\":{\"layer1\":{\"columnOrder\":[\"col2\"],\"columns\":{\"col2\":{\"dataType\":\"number\",\"isBucketed\":false,\"label\":\"Count of records\",\"operationType\":\"count\",\"scale\":\"ratio\",\"sourceField\":\"Records\"}}}}}},\"visualization\":{\"layerId\":\"layer1\",\"accessor\":\"col2\"},\"query\":{\"language\":\"kuery\",\"query\":\"\"},\"filters\":[]},\"references\":[{\"type\":\"index-pattern\",\"id\":\"90943e30-9a47-11e8-b64d-95841ca0b247\",\"name\":\"indexpattern-datasource-current-indexpattern\"},{\"type\":\"index-pattern\",\"id\":\"90943e30-9a47-11e8-b64d-95841ca0b247\",\"name\":\"indexpattern-datasource-layer-layer1\"}]}}}\n\nbrbrbr"`, + type: 'user', + created_at: '2021-07-19T08:41:29.951Z', + created_by: { + email: null, + full_name: null, + username: 'elastic', + }, + pushed_at: null, + pushed_by: null, + updated_at: '2021-07-19T08:41:47.549Z', + updated_by: { + full_name: null, + email: null, + username: 'elastic', + }, + }, + references: [ + { + name: 'associated-cases', + id: '77d1b230-d35e-11eb-8da6-6f746b9cb499', + type: 'cases', + }, + { + name: 'indexpattern-datasource-current-indexpattern', + id: '90943e30-9a47-11e8-b64d-95841ca0b247', + type: 'index-pattern', + }, + { + name: 'indexpattern-datasource-current-indexpattern', + id: '90943e30-9a47-11e8-b64d-95841ca0b247', + type: 'index-pattern', + }, + ], + migrationVersion: { + 'cases-comments': '7.14.0', + }, + coreMigrationVersion: '8.0.0', + updated_at: '2021-07-19T08:41:47.552Z', + version: 'WzgxMTY4MSw5XQ==', + namespaces: ['default'], + score: 0, + }; + + it('should remove time zone param from date histogram', () => { + expect(migrations['7.14.0']).toBeDefined(); + const result = migrations['7.14.0'](caseComment, contextMock); + + const parsedComment = parseCommentString(result.attributes.comment); + const lensVisualizations = getLensVisualizations(parsedComment.children); + + const layers = Object.values( + lensVisualizations[0].attributes.state.datasourceStates.indexpattern.layers + ); + expect(result.attributes.comment).toEqual(expectedMigrationCommentResult); + expect(layers.length).toBe(1); + const columns = Object.values(layers[0].columns); + expect(columns.length).toBe(3); + expect(columns[0].operationType).toEqual('date_histogram'); + expect((columns[0] as { params: {} }).params).toEqual({ interval: 'auto' }); + expect(columns[1].operationType).toEqual('date_histogram'); + expect((columns[1] as { params: {} }).params).toEqual({ interval: 'auto' }); + expect(columns[2].operationType).toEqual('my_unexpected_operation'); + expect((columns[2] as { params: {} }).params).toEqual({ timeZone: 'do not delete' }); + }); + }); +}); diff --git a/x-pack/plugins/cases/server/saved_object_types/migrations/index.ts b/x-pack/plugins/cases/server/saved_object_types/migrations/index.ts index 7be87c3abc989..b1792d98cfdb2 100644 --- a/x-pack/plugins/cases/server/saved_object_types/migrations/index.ts +++ b/x-pack/plugins/cases/server/saved_object_types/migrations/index.ts @@ -7,9 +7,19 @@ /* eslint-disable @typescript-eslint/naming-convention */ +import { flow, mapValues } from 'lodash'; +import { LensServerPluginSetup } from '../../../../lens/server'; + +import { + mergeMigrationFunctionMaps, + MigrateFunction, + MigrateFunctionsObject, +} from '../../../../../../src/plugins/kibana_utils/common'; import { SavedObjectUnsanitizedDoc, SavedObjectSanitizedDoc, + SavedObjectMigrationFn, + SavedObjectMigrationMap, } from '../../../../../../src/core/server'; import { ConnectorTypes, @@ -17,6 +27,7 @@ import { AssociationType, SECURITY_SOLUTION_OWNER, } from '../../../common'; +import { parseCommentString, stringifyComment } from '../../common'; export { caseMigrations } from './cases'; export { configureMigrations } from './configuration'; @@ -103,44 +114,86 @@ interface SanitizedCommentForSubCases { rule?: { id: string | null; name: string | null }; } -export const commentsMigrations = { - '7.11.0': ( - doc: SavedObjectUnsanitizedDoc - ): SavedObjectSanitizedDoc => { - return { - ...doc, - attributes: { - ...doc.attributes, - type: CommentType.user, - }, - references: doc.references || [], - }; - }, - '7.12.0': ( - doc: SavedObjectUnsanitizedDoc - ): SavedObjectSanitizedDoc => { - let attributes: SanitizedCommentForSubCases & UnsanitizedComment = { - ...doc.attributes, - associationType: AssociationType.case, - }; - - // only add the rule object for alert comments. Prior to 7.12 we only had CommentType.alert, generated alerts are - // introduced in 7.12. - if (doc.attributes.type === CommentType.alert) { - attributes = { ...attributes, rule: { id: null, name: null } }; +const migrateByValueLensVisualizations = ( + migrate: MigrateFunction, + version: string +): SavedObjectMigrationFn => (doc: any) => { + const parsedComment = parseCommentString(doc.attributes.comment); + const migratedComment = parsedComment.children.map((comment) => { + if (comment?.type === 'lens') { + // @ts-expect-error + return migrate(comment); } - return { - ...doc, - attributes, - references: doc.references || [], - }; - }, - '7.14.0': ( - doc: SavedObjectUnsanitizedDoc> - ): SavedObjectSanitizedDoc => { - return addOwnerToSO(doc); - }, + return comment; + }); + + // @ts-expect-error + parsedComment.children = migratedComment; + doc.attributes.comment = stringifyComment(parsedComment); + + return doc; +}; + +export interface CreateCommentsMigrationsDeps { + lensEmbeddableFactory: LensServerPluginSetup['lensEmbeddableFactory']; +} + +export const createCommentsMigrations = ( + migrationDeps: CreateCommentsMigrationsDeps +): SavedObjectMigrationMap => { + const embeddableMigrations = mapValues( + migrationDeps.lensEmbeddableFactory().migrations, + migrateByValueLensVisualizations + ) as MigrateFunctionsObject; + + const commentsMigrations = { + '7.11.0': flow( + ( + doc: SavedObjectUnsanitizedDoc + ): SavedObjectSanitizedDoc => { + return { + ...doc, + attributes: { + ...doc.attributes, + type: CommentType.user, + }, + references: doc.references || [], + }; + } + ), + '7.12.0': flow( + ( + doc: SavedObjectUnsanitizedDoc + ): SavedObjectSanitizedDoc => { + let attributes: SanitizedCommentForSubCases & UnsanitizedComment = { + ...doc.attributes, + associationType: AssociationType.case, + }; + + // only add the rule object for alert comments. Prior to 7.12 we only had CommentType.alert, generated alerts are + // introduced in 7.12. + if (doc.attributes.type === CommentType.alert) { + attributes = { ...attributes, rule: { id: null, name: null } }; + } + + return { + ...doc, + attributes, + references: doc.references || [], + }; + } + ), + '7.14.0': flow( + ( + doc: SavedObjectUnsanitizedDoc> + ): SavedObjectSanitizedDoc => { + return addOwnerToSO(doc); + } + ), + }; + + return mergeMigrationFunctionMaps(commentsMigrations, embeddableMigrations); }; export const connectorMappingsMigrations = { diff --git a/x-pack/plugins/cases/server/services/attachments/index.ts b/x-pack/plugins/cases/server/services/attachments/index.ts index c2d9b4826fc14..105b6a3125523 100644 --- a/x-pack/plugins/cases/server/services/attachments/index.ts +++ b/x-pack/plugins/cases/server/services/attachments/index.ts @@ -5,7 +5,12 @@ * 2.0. */ -import { Logger, SavedObject, SavedObjectReference } from 'kibana/server'; +import { + Logger, + SavedObject, + SavedObjectReference, + SavedObjectsUpdateOptions, +} from 'kibana/server'; import { KueryNode } from '../../../../../../src/plugins/data/common'; import { @@ -38,10 +43,10 @@ interface CreateAttachmentArgs extends ClientArgs { interface UpdateArgs { attachmentId: string; updatedAttributes: AttachmentPatchAttributes; - version?: string; + options?: SavedObjectsUpdateOptions; } -type UpdateAttachmentArgs = UpdateArgs & ClientArgs; +export type UpdateAttachmentArgs = UpdateArgs & ClientArgs; interface BulkUpdateAttachmentArgs extends ClientArgs { comments: UpdateArgs[]; @@ -142,7 +147,7 @@ export class AttachmentService { unsecuredSavedObjectsClient, attachmentId, updatedAttributes, - version, + options, }: UpdateAttachmentArgs) { try { this.log.debug(`Attempting to UPDATE comment ${attachmentId}`); @@ -150,7 +155,7 @@ export class AttachmentService { CASE_COMMENT_SAVED_OBJECT, attachmentId, updatedAttributes, - { version } + options ); } catch (error) { this.log.error(`Error on UPDATE comment ${attachmentId}: ${error}`); @@ -168,7 +173,7 @@ export class AttachmentService { type: CASE_COMMENT_SAVED_OBJECT, id: c.attachmentId, attributes: c.updatedAttributes, - version: c.version, + ...c.options, })) ); } catch (error) { diff --git a/x-pack/plugins/cases/tsconfig.json b/x-pack/plugins/cases/tsconfig.json index 99622df805ced..1c9373e023366 100644 --- a/x-pack/plugins/cases/tsconfig.json +++ b/x-pack/plugins/cases/tsconfig.json @@ -16,6 +16,7 @@ { "path": "../../../src/core/tsconfig.json" }, // optionalPlugins from ./kibana.json + { "path": "../lens/tsconfig.json" }, { "path": "../security/tsconfig.json" }, { "path": "../spaces/tsconfig.json" }, @@ -24,6 +25,7 @@ { "path": "../triggers_actions_ui/tsconfig.json"}, { "path": "../../../src/plugins/es_ui_shared/tsconfig.json" }, { "path": "../../../src/plugins/kibana_react/tsconfig.json" }, - { "path": "../../../src/plugins/kibana_utils/tsconfig.json" } + { "path": "../../../src/plugins/kibana_utils/tsconfig.json" }, + { "path": "../../../src/plugins/saved_objects/tsconfig.json" } ] } diff --git a/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/hooks/use_data_viz_chart_theme.ts b/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/hooks/use_data_viz_chart_theme.ts index ad31ca2d09420..56b5049786aca 100644 --- a/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/hooks/use_data_viz_chart_theme.ts +++ b/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/hooks/use_data_viz_chart_theme.ts @@ -10,7 +10,7 @@ import { useMemo } from 'react'; import { useCurrentEuiTheme } from './use_color_range'; export const useDataVizChartTheme = (): PartialTheme => { const { euiTheme } = useCurrentEuiTheme(); - const chartTheme = useMemo(() => { + const chartTheme = useMemo(() => { const AREA_SERIES_COLOR = euiTheme.euiColorVis0; return { axes: { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/deduplication_panel/deduplication_panel.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/deduplication_panel/deduplication_panel.test.tsx index eba4ee9423ae9..9c076c5550a34 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/deduplication_panel/deduplication_panel.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/deduplication_panel/deduplication_panel.test.tsx @@ -26,6 +26,8 @@ import { import { mountWithIntl, rerender } from '../../../../../test_helpers'; +import { DataPanel } from '../../../data_panel'; + import { DeduplicationPanel } from './deduplication_panel'; const MOCK_ACTIONS = { @@ -60,7 +62,7 @@ describe('DeduplicationPanel', () => { it('contains a button to reset to defaults', () => { const wrapper = shallow(); - wrapper.find(EuiButton).simulate('click'); + wrapper.find(DataPanel).dive().find(EuiButton).simulate('click'); expect(MOCK_ACTIONS.submitDeduplicationUpdate).toHaveBeenCalledWith(MOCK_VALUES.domain, { fields: [], diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/deduplication_panel/deduplication_panel.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/deduplication_panel/deduplication_panel.tsx index 1686b5e25e8f0..a25583f91763e 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/deduplication_panel/deduplication_panel.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/deduplication_panel/deduplication_panel.tsx @@ -21,8 +21,6 @@ import { EuiSelectable, EuiSpacer, EuiSwitch, - EuiText, - EuiTitle, } from '@elastic/eui'; import { EuiSelectableLIOption } from '@elastic/eui/src/components/selectable/selectable_option'; @@ -30,6 +28,7 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { DOCS_PREFIX } from '../../../../routes'; +import { DataPanel } from '../../../data_panel'; import { CrawlerSingleDomainLogic } from '../../crawler_single_domain_logic'; import { getCheckedOptionLabels, getSelectableOptions } from './utils'; @@ -52,62 +51,56 @@ export const DeduplicationPanel: React.FC = () => { const selectableOptions = getSelectableOptions(domain, showAllFields); return ( -
- - - -

- {i18n.translate('xpack.enterpriseSearch.appSearch.crawler.deduplicationPanel.title', { - defaultMessage: 'Duplicate document handling', - })} -

-
-
- - submitDeduplicationUpdate(domain, { fields: [] })} - disabled={deduplicationFields.length === 0} - > - {i18n.translate( - 'xpack.enterpriseSearch.appSearch.crawler.deduplicationPanel.resetToDefaultsButtonLabel', - { - defaultMessage: 'Reset to defaults', - } - )} - - -
- - -

- - {i18n.translate( - 'xpack.enterpriseSearch.appSearch.crawler.deduplicationPanel.learnMoreMessage', - { - defaultMessage: 'Learn more about content hashing', - } - )} - - ), - }} - /> -

-
- + + {i18n.translate('xpack.enterpriseSearch.appSearch.crawler.deduplicationPanel.title', { + defaultMessage: 'Duplicate document handling', + })} + + } + action={ + submitDeduplicationUpdate(domain, { fields: [] })} + disabled={deduplicationFields.length === 0} + > + {i18n.translate( + 'xpack.enterpriseSearch.appSearch.crawler.deduplicationPanel.resetToDefaultsButtonLabel', + { + defaultMessage: 'Reset to defaults', + } + )} + + } + subtitle={ + + {i18n.translate( + 'xpack.enterpriseSearch.appSearch.crawler.deduplicationPanel.learnMoreMessage', + { + defaultMessage: 'Learn more about content hashing', + } + )} + + ), + }} + /> + } + > { : submitDeduplicationUpdate(domain, { enabled: true }) } /> +
@@ -209,6 +203,6 @@ export const DeduplicationPanel: React.FC = () => {
-
+ ); }; diff --git a/x-pack/plugins/fleet/server/routes/package_policy/handlers.ts b/x-pack/plugins/fleet/server/routes/package_policy/handlers.ts index 88e200e90467a..b9f0f6c69d4e7 100644 --- a/x-pack/plugins/fleet/server/routes/package_policy/handlers.ts +++ b/x-pack/plugins/fleet/server/routes/package_policy/handlers.ts @@ -203,7 +203,11 @@ export const upgradePackagePolicyHandler: RequestHandler< const body: UpgradePackagePolicyDryRunResponse = []; for (const id of request.body.packagePolicyIds) { - const result = await packagePolicyService.getUpgradeDryRunDiff(soClient, id); + const result = await packagePolicyService.getUpgradeDryRunDiff( + soClient, + id, + request.body.packageVersion + ); body.push(result); } return response.ok({ diff --git a/x-pack/plugins/fleet/server/services/package_policy.ts b/x-pack/plugins/fleet/server/services/package_policy.ts index 573e1847f8eb3..561cbef952f8d 100644 --- a/x-pack/plugins/fleet/server/services/package_policy.ts +++ b/x-pack/plugins/fleet/server/services/package_policy.ts @@ -7,6 +7,7 @@ import { omit } from 'lodash'; import { i18n } from '@kbn/i18n'; +import { getFlattenedObject } from '@kbn/std'; import type { KibanaRequest } from 'src/core/server'; import type { ElasticsearchClient, @@ -21,6 +22,8 @@ import { packageToPackagePolicyInputs, isPackageLimited, doesAgentPolicyAlreadyIncludePackage, + validatePackagePolicy, + validationHasErrors, } from '../../common'; import type { DeletePackagePoliciesResponse, @@ -442,7 +445,11 @@ class PackagePolicyService { return result; } - public async getUpgradePackagePolicyInfo(soClient: SavedObjectsClientContract, id: string) { + public async getUpgradePackagePolicyInfo( + soClient: SavedObjectsClientContract, + id: string, + packageVersion?: string + ) { const packagePolicy = await this.get(soClient, id); if (!packagePolicy) { throw new Error( @@ -462,28 +469,30 @@ class PackagePolicyService { ); } - const installedPackage = await getInstallation({ - savedObjectsClient: soClient, - pkgName: packagePolicy.package.name, - }); - if (!installedPackage) { - throw new Error( - i18n.translate('xpack.fleet.packagePolicy.packageNotInstalledError', { - defaultMessage: 'Cannot upgrade package policy {id} because {pkgName} is not installed', - values: { id, pkgName: packagePolicy.package.name }, - }) - ); - } + let packageInfo: PackageInfo; - const installedPkgInfo = await getPackageInfo({ - savedObjectsClient: soClient, - pkgName: packagePolicy.package.name, - pkgVersion: installedPackage.version, - }); + if (packageVersion) { + packageInfo = await getPackageInfo({ + savedObjectsClient: soClient, + pkgName: packagePolicy.package.name, + pkgVersion: packageVersion, + }); + } else { + const installedPackage = await getInstallation({ + savedObjectsClient: soClient, + pkgName: packagePolicy.package.name, + }); + + packageInfo = await getPackageInfo({ + savedObjectsClient: soClient, + pkgName: packagePolicy.package.name, + pkgVersion: installedPackage?.version ?? '', + }); + } return { packagePolicy: packagePolicy as Required, - installedPkgInfo, + packageInfo, }; } @@ -497,25 +506,23 @@ class PackagePolicyService { for (const id of ids) { try { - const { packagePolicy, installedPkgInfo } = await this.getUpgradePackagePolicyInfo( - soClient, - id - ); + const { packagePolicy, packageInfo } = await this.getUpgradePackagePolicyInfo(soClient, id); const updatePackagePolicy = overridePackageInputs( { ...omit(packagePolicy, 'id'), - inputs: packageToPackagePolicyInputs(installedPkgInfo), + inputs: packagePolicy.inputs, package: { ...packagePolicy.package, - version: installedPkgInfo.version, + version: packageInfo.version, }, }, - packagePolicy.inputs as InputsOverride[] + packageInfo, + packageToPackagePolicyInputs(packageInfo) as InputsOverride[] ); updatePackagePolicy.inputs = await this.compilePackagePolicyInputs( - installedPkgInfo, + packageInfo, updatePackagePolicy.vars || {}, updatePackagePolicy.inputs as PackagePolicyInput[] ); @@ -546,29 +553,32 @@ class PackagePolicyService { public async getUpgradeDryRunDiff( soClient: SavedObjectsClientContract, - id: string + id: string, + packageVersion?: string ): Promise { try { - const { packagePolicy, installedPkgInfo } = await this.getUpgradePackagePolicyInfo( + const { packagePolicy, packageInfo } = await this.getUpgradePackagePolicyInfo( soClient, - id + id, + packageVersion ); const updatedPackagePolicy = overridePackageInputs( { ...omit(packagePolicy, 'id'), - inputs: packageToPackagePolicyInputs(installedPkgInfo), + inputs: packagePolicy.inputs, package: { ...packagePolicy.package, - version: installedPkgInfo.version, + version: packageInfo.version, }, }, - packagePolicy.inputs as InputsOverride[], + packageInfo, + packageToPackagePolicyInputs(packageInfo) as InputsOverride[], true ); updatedPackagePolicy.inputs = await this.compilePackagePolicyInputs( - installedPkgInfo, + packageInfo, updatedPackagePolicy.vars || {}, updatedPackagePolicy.inputs as PackagePolicyInput[] ); @@ -849,6 +859,7 @@ export type { PackagePolicyService }; export function overridePackageInputs( basePackagePolicy: NewPackagePolicy, + packageInfo: PackageInfo, inputsOverride?: InputsOverride[], dryRun?: boolean ): DryRunPackagePolicy { @@ -856,11 +867,11 @@ export function overridePackageInputs( const inputs = [...basePackagePolicy.inputs]; const packageName = basePackagePolicy.package!.name; - const errors = []; - let responseMissingVars: string[] = []; + let errors = []; for (const override of inputsOverride) { let originalInput = inputs.find((i) => i.type === override.type); + if (!originalInput) { const e = { error: new Error( @@ -874,13 +885,16 @@ export function overridePackageInputs( ), package: { name: packageName, version: basePackagePolicy.package!.version }, }; + if (dryRun) { errors.push({ key: override.type, message: String(e.error), }); continue; - } else throw e; + } else { + throw e; + } } if (typeof override.enabled !== 'undefined') originalInput.enabled = override.enabled; @@ -888,33 +902,7 @@ export function overridePackageInputs( originalInput.keep_enabled = override.keep_enabled; if (override.vars) { - try { - const { result, missingVars } = deepMergeVars(override, originalInput); - originalInput = result; - responseMissingVars = [...responseMissingVars, ...missingVars]; - } catch (e) { - const varName = e.message; - const err = { - error: new Error( - i18n.translate('xpack.fleet.packagePolicyVarOverrideError', { - defaultMessage: - 'Var {varName} does not exist on {inputType} of package {packageName}', - values: { - varName, - inputType: override.type, - packageName, - }, - }) - ), - package: { name: packageName, version: basePackagePolicy.package!.version }, - }; - if (dryRun) { - errors.push({ - key: `${override.type}.vars.${varName}`, - message: String(err.error), - }); - } else throw err; - } + originalInput = deepMergeVars(originalInput, override); } if (override.streams) { @@ -922,6 +910,7 @@ export function overridePackageInputs( let originalStream = originalInput?.streams.find( (s) => s.data_stream.dataset === stream.data_stream.dataset ); + if (!originalStream) { const streamSet = stream.data_stream.dataset; const e = { @@ -938,62 +927,61 @@ export function overridePackageInputs( ), package: { name: packageName, version: basePackagePolicy.package!.version }, }; + if (dryRun) { errors.push({ key: `${override.type}.streams.${streamSet}`, message: String(e.error), }); + continue; - } else throw e; + } else { + throw e; + } } - if (typeof stream.enabled !== 'undefined') originalStream.enabled = stream.enabled; + if (typeof stream.enabled !== 'undefined') { + originalStream.enabled = stream.enabled; + } if (stream.vars) { - try { - const { result, missingVars } = deepMergeVars(stream as InputsOverride, originalStream); - originalStream = result; - responseMissingVars = [...responseMissingVars, ...missingVars]; - } catch (e) { - const varName = e.message; - const streamSet = stream.data_stream.dataset; - const err = { - error: new Error( - i18n.translate('xpack.fleet.packagePolicyStreamVarOverrideError', { - defaultMessage: - 'Var {varName} does not exist on {streamSet} for {inputType} of package {packageName}', - values: { - varName, - streamSet, - inputType: override.type, - packageName, - }, - }) - ), - package: { name: packageName, version: basePackagePolicy.package!.version }, - }; - if (dryRun) { - errors.push({ - key: `${override.type}.streams.${streamSet}.${varName}`, - message: String(err.error), - }); - } else throw err; - } + originalStream = deepMergeVars(originalStream, stream as InputsOverride); } } } } + const resultingPackagePolicy: NewPackagePolicy = { + ...basePackagePolicy, + inputs, + }; + + const validationResults = validatePackagePolicy(resultingPackagePolicy, packageInfo); + + if (validationHasErrors(validationResults)) { + const responseFormattedValidationErrors = Object.entries(getFlattenedObject(validationResults)) + .map(([key, value]) => ({ + key, + message: value, + })) + .filter(({ message }) => !!message); + + errors = [...errors, ...responseFormattedValidationErrors]; + } + if (dryRun && errors.length) { - return { ...basePackagePolicy, inputs, errors, missingVars: responseMissingVars }; + return { ...resultingPackagePolicy, errors }; } - return { ...basePackagePolicy, inputs, missingVars: responseMissingVars }; + return resultingPackagePolicy; } -function deepMergeVars(override: any, original: any): { result: any; missingVars: string[] } { +function deepMergeVars(original: any, override: any): any { const result = { ...original }; - const missingVars: string[] = []; + + if (!result.vars || !override.vars) { + return; + } const overrideVars = Array.isArray(override.vars) ? override.vars @@ -1002,15 +990,15 @@ function deepMergeVars(override: any, original: any): { result: any; missingVars ...(rest as any), })); - for (const { name, ...val } of overrideVars) { - if (!original.vars || !(name in original.vars)) { - missingVars.push(name); - continue; + for (const { name, ...overrideVal } of overrideVars) { + const originalVar = original.vars[name]; + + if (!result.vars) { + result.vars = {}; } - const originalVar = original.vars[name]; - result[name] = { ...originalVar, ...val }; + result.vars[name] = { ...overrideVal, ...originalVar }; } - return { result, missingVars }; + return result; } diff --git a/x-pack/plugins/fleet/server/services/preconfiguration.ts b/x-pack/plugins/fleet/server/services/preconfiguration.ts index 334df17a8d3a8..37ed98a6f4aa0 100644 --- a/x-pack/plugins/fleet/server/services/preconfiguration.ts +++ b/x-pack/plugins/fleet/server/services/preconfiguration.ts @@ -28,7 +28,7 @@ import { import { escapeSearchQueryPhrase } from './saved_object'; import { pkgToPkgKey } from './epm/registry'; -import { getInstallation } from './epm/packages'; +import { getInstallation, getPackageInfo } from './epm/packages'; import { ensurePackagesCompletedInstall } from './epm/packages/install'; import { bulkInstallPackages } from './epm/packages/bulk_install_packages'; import { agentPolicyService, addPackageToAgentPolicy } from './agent_policy'; @@ -277,6 +277,12 @@ async function addPreconfiguredPolicyPackages( ) { // Add packages synchronously to avoid overwriting for (const { installedPackage, name, description, inputs } of installedPackagePolicies) { + const packageInfo = await getPackageInfo({ + savedObjectsClient: soClient, + pkgName: installedPackage.name, + pkgVersion: installedPackage.version, + }); + await addPackageToAgentPolicy( soClient, esClient, @@ -285,7 +291,7 @@ async function addPreconfiguredPolicyPackages( defaultOutput, name, description, - (policy) => overridePackageInputs(policy, inputs) + (policy) => overridePackageInputs(policy, packageInfo, inputs) ); } } diff --git a/x-pack/plugins/fleet/server/types/rest_spec/package_policy.ts b/x-pack/plugins/fleet/server/types/rest_spec/package_policy.ts index a88316e8e7574..4ccc57aca0ebd 100644 --- a/x-pack/plugins/fleet/server/types/rest_spec/package_policy.ts +++ b/x-pack/plugins/fleet/server/types/rest_spec/package_policy.ts @@ -41,5 +41,6 @@ export const UpgradePackagePoliciesRequestSchema = { body: schema.object({ packagePolicyIds: schema.arrayOf(schema.string()), dryRun: schema.maybe(schema.boolean()), + packageVersion: schema.maybe(schema.string()), }), }; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/processors/dot_expander.test.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/processors/dot_expander.test.tsx new file mode 100644 index 0000000000000..75468f31b1a54 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/processors/dot_expander.test.tsx @@ -0,0 +1,122 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { act } from 'react-dom/test-utils'; +import { setup, SetupResult, getProcessorValue } from './processor.helpers'; + +const DOT_EXPANDER_TYPE = 'dot_expander'; + +describe('Processor: Dot Expander', () => { + let onUpdate: jest.Mock; + let testBed: SetupResult; + + beforeAll(() => { + jest.useFakeTimers(); + }); + + afterAll(() => { + jest.useRealTimers(); + }); + + beforeEach(async () => { + onUpdate = jest.fn(); + + await act(async () => { + testBed = await setup({ + value: { + processors: [], + }, + onFlyoutOpen: jest.fn(), + onUpdate, + }); + }); + + testBed.component.update(); + + // Open flyout to add new processor + testBed.actions.addProcessor(); + // Add type (the other fields are not visible until a type is selected) + await testBed.actions.addProcessorType(DOT_EXPANDER_TYPE); + }); + + test('prevents form submission if required fields are not provided', async () => { + const { + actions: { saveNewProcessor }, + form, + } = testBed; + + // Click submit button with only the type defined + await saveNewProcessor(); + + // Expect form error as "field" is a required parameter + expect(form.getErrorsMessages()).toEqual(['A field value is required.']); + }); + + test('prevents form submission if field does not contain a . for the dot notation', async () => { + const { + actions: { saveNewProcessor }, + form, + component, + } = testBed; + + // Add invalid "field" value (required) + form.setInputValue('fieldNameField.input', 'missingTheDot'); + + // Save the processor with invalid field + await saveNewProcessor(); + + // Move ahead the debounce time which will then execute any validations + await act(async () => { + jest.runAllTimers(); + }); + component.update(); + + // Expect form error as "field" does not contain '.' + expect(form.getErrorsMessages()).toEqual([ + 'A field value requires at least one dot character.', + ]); + }); + test('saves with default parameter values', async () => { + const { + actions: { saveNewProcessor }, + form, + } = testBed; + + // Add "field" value (required) + form.setInputValue('fieldNameField.input', 'field.with.dot'); + + // Save the field + await saveNewProcessor(); + + const processors = getProcessorValue(onUpdate, DOT_EXPANDER_TYPE); + expect(processors[0][DOT_EXPANDER_TYPE]).toEqual({ + field: 'field.with.dot', + }); + }); + + test('allows optional parameters to be set', async () => { + const { + actions: { saveNewProcessor }, + form, + } = testBed; + + // Add "field" value (required) + form.setInputValue('fieldNameField.input', 'field.notation'); + + // Set optional parameters + form.setInputValue('pathField.input', 'somepath'); + + // Save the field with new changes + await saveNewProcessor(); + + const processors = getProcessorValue(onUpdate, DOT_EXPANDER_TYPE); + expect(processors[0][DOT_EXPANDER_TYPE]).toEqual({ + field: 'field.notation', + path: 'somepath', + }); + }); +}); diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/processors/processor.helpers.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/processors/processor.helpers.tsx index 9101e64278dc6..65d9b8f306058 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/processors/processor.helpers.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/__jest__/processors/processor.helpers.tsx @@ -146,6 +146,7 @@ type TestSubject = | 'fieldNameField.input' | 'messageField.input' | 'mockCodeEditor' + | 'pathField.input' | 'tagField.input' | 'typeSelectorField' | 'dateRoundingField' diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/dot_expander.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/dot_expander.tsx index 4bbc242cf0ef8..c66633dfd23d5 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/dot_expander.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_editor/components/processor_form/processors/dot_expander.tsx @@ -54,7 +54,12 @@ export const DotExpander: FunctionComponent = () => { ]} /> - + ); }; diff --git a/x-pack/plugins/lens/common/embeddable_factory/index.ts b/x-pack/plugins/lens/common/embeddable_factory/index.ts new file mode 100644 index 0000000000000..1eaa1dddfdf08 --- /dev/null +++ b/x-pack/plugins/lens/common/embeddable_factory/index.ts @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { SerializableRecord, Serializable } from '@kbn/utility-types'; +import { SavedObjectReference } from 'src/core/types'; +import { EmbeddableStateWithType } from 'src/plugins/embeddable/common'; +import { EmbeddableRegistryDefinition } from 'src/plugins/embeddable/server'; + +export type LensEmbeddablePersistableState = EmbeddableStateWithType & { + attributes: SerializableRecord; +}; + +export const inject: EmbeddableRegistryDefinition['inject'] = (state, references) => { + const typedState = state as LensEmbeddablePersistableState; + + if ('attributes' in typedState && typedState.attributes !== undefined) { + typedState.attributes.references = (references as unknown) as Serializable[]; + } + + return typedState; +}; + +export const extract: EmbeddableRegistryDefinition['extract'] = (state) => { + let references: SavedObjectReference[] = []; + const typedState = state as LensEmbeddablePersistableState; + + if ('attributes' in typedState && typedState.attributes !== undefined) { + references = (typedState.attributes.references as unknown) as SavedObjectReference[]; + } + + return { state, references }; +}; diff --git a/x-pack/plugins/lens/public/app_plugin/mounter.tsx b/x-pack/plugins/lens/public/app_plugin/mounter.tsx index 5a783bc4180d3..6bbc1284a0f1e 100644 --- a/x-pack/plugins/lens/public/app_plugin/mounter.tsx +++ b/x-pack/plugins/lens/public/app_plugin/mounter.tsx @@ -147,6 +147,7 @@ export async function mountApp( if (stateTransfer && props?.input) { const { input, isCopied } = props; stateTransfer.navigateToWithEmbeddablePackage(embeddableEditorIncomingState?.originatingApp, { + path: embeddableEditorIncomingState?.originatingPath, state: { embeddableId: isCopied ? undefined : embeddableEditorIncomingState.embeddableId, type: LENS_EMBEDDABLE_TYPE, @@ -155,7 +156,9 @@ export async function mountApp( }, }); } else { - coreStart.application.navigateToApp(embeddableEditorIncomingState?.originatingApp); + coreStart.application.navigateToApp(embeddableEditorIncomingState?.originatingApp, { + path: embeddableEditorIncomingState?.originatingPath, + }); } }; const initialContext = diff --git a/x-pack/plugins/lens/public/embeddable/embeddable_factory.ts b/x-pack/plugins/lens/public/embeddable/embeddable_factory.ts index 4cc074b5e830c..dcb72455e0ee9 100644 --- a/x-pack/plugins/lens/public/embeddable/embeddable_factory.ts +++ b/x-pack/plugins/lens/public/embeddable/embeddable_factory.ts @@ -5,11 +5,10 @@ * 2.0. */ -import type { Capabilities, HttpSetup, SavedObjectReference } from 'kibana/public'; +import type { Capabilities, HttpSetup } from 'kibana/public'; import { i18n } from '@kbn/i18n'; import { RecursiveReadonly } from '@kbn/utility-types'; import { Ast } from '@kbn/interpreter/target/common'; -import { EmbeddableStateWithType } from 'src/plugins/embeddable/common'; import { UsageCollectionSetup } from 'src/plugins/usage_collection/public'; import { IndexPatternsContract, TimefilterContract } from '../../../../../src/plugins/data/public'; import { ReactExpressionRendererType } from '../../../../../src/plugins/expressions/public'; @@ -23,6 +22,7 @@ import { Document } from '../persistence/saved_object_store'; import { LensAttributeService } from '../lens_attribute_service'; import { DOC_TYPE } from '../../common'; import { ErrorMessage } from '../editor_frame_service/types'; +import { extract, inject } from '../../common/embeddable_factory'; export interface LensEmbeddableStartServices { timefilter: TimefilterContract; @@ -112,14 +112,6 @@ export class EmbeddableFactory implements EmbeddableFactoryDefinition { ); } - extract(state: EmbeddableStateWithType) { - let references: SavedObjectReference[] = []; - const typedState = (state as unknown) as LensEmbeddableInput; - - if ('attributes' in typedState && typedState.attributes !== undefined) { - references = typedState.attributes.references; - } - - return { state, references }; - } + extract = extract; + inject = inject; } diff --git a/x-pack/plugins/lens/public/heatmap_visualization/chart_component.tsx b/x-pack/plugins/lens/public/heatmap_visualization/chart_component.tsx index e3da4bfe7fe72..d38afc17b2b07 100644 --- a/x-pack/plugins/lens/public/heatmap_visualization/chart_component.tsx +++ b/x-pack/plugins/lens/public/heatmap_visualization/chart_component.tsx @@ -285,7 +285,7 @@ export const HeatmapComponent: FC = ({ yAxisLabel: { visible: !!yAxisColumn && args.gridConfig.isYAxisLabelVisible, // eui color subdued - fill: chartTheme.axes?.tickLabel?.fill ?? '#6a717d', + textColor: chartTheme.axes?.tickLabel?.fill ?? '#6a717d', padding: yAxisColumn?.name ? 8 : 0, name: yAxisColumn?.name ?? '', ...(yAxisColumn @@ -297,7 +297,7 @@ export const HeatmapComponent: FC = ({ xAxisLabel: { visible: args.gridConfig.isXAxisLabelVisible, // eui color subdued - fill: chartTheme.axes?.tickLabel?.fill ?? `#6a717d`, + textColor: chartTheme.axes?.tickLabel?.fill ?? `#6a717d`, formatter: (v: number | string) => xValuesFormatter.convert(v), name: xAxisColumn.name, }, diff --git a/x-pack/plugins/lens/public/plugin.ts b/x-pack/plugins/lens/public/plugin.ts index e0a4848974237..6e8b7d35b0cb9 100644 --- a/x-pack/plugins/lens/public/plugin.ts +++ b/x-pack/plugins/lens/public/plugin.ts @@ -130,7 +130,14 @@ export interface LensPublicStart { * * @experimental */ - navigateToPrefilledEditor: (input: LensEmbeddableInput, openInNewTab?: boolean) => void; + navigateToPrefilledEditor: ( + input: LensEmbeddableInput | undefined, + options?: { + openInNewTab?: boolean; + originatingApp?: string; + originatingPath?: string; + } + ) => void; /** * Method which returns true if the user has permission to use Lens as defined by application capabilities. */ @@ -336,20 +343,24 @@ export class LensPlugin { return { EmbeddableComponent: getEmbeddableComponent(core, startDependencies), SaveModalComponent: getSaveModalComponent(core, startDependencies, this.attributeService!), - navigateToPrefilledEditor: (input: LensEmbeddableInput, openInNewTab?: boolean) => { + navigateToPrefilledEditor: ( + input, + { openInNewTab = false, originatingApp = '', originatingPath } = {} + ) => { // for openInNewTab, we set the time range in url via getEditPath below - if (input.timeRange && !openInNewTab) { + if (input?.timeRange && !openInNewTab) { startDependencies.data.query.timefilter.timefilter.setTime(input.timeRange); } const transfer = new EmbeddableStateTransfer( core.application.navigateToApp, core.application.currentAppId$ ); - transfer.navigateToEditor('lens', { + transfer.navigateToEditor(APP_ID, { openInNewTab, - path: getEditPath(undefined, openInNewTab ? input.timeRange : undefined), + path: getEditPath(undefined, (openInNewTab && input?.timeRange) || undefined), state: { - originatingApp: '', + originatingApp, + originatingPath, valueInput: input, }, }); diff --git a/x-pack/plugins/lens/server/embeddable/lens_embeddable_factory.ts b/x-pack/plugins/lens/server/embeddable/lens_embeddable_factory.ts index 14a9713d8461e..86a3a600b58ab 100644 --- a/x-pack/plugins/lens/server/embeddable/lens_embeddable_factory.ts +++ b/x-pack/plugins/lens/server/embeddable/lens_embeddable_factory.ts @@ -19,6 +19,7 @@ import { LensDocShapePre712, VisStatePre715, } from '../migrations/types'; +import { extract, inject } from '../../common/embeddable_factory'; export const lensEmbeddableFactory = (): EmbeddableRegistryDefinition => { return { @@ -50,5 +51,7 @@ export const lensEmbeddableFactory = (): EmbeddableRegistryDefinition => { } as unknown) as SerializableRecord; }, }, + extract, + inject, }; }; diff --git a/x-pack/plugins/lens/server/index.ts b/x-pack/plugins/lens/server/index.ts index b61282c9e26e5..f8a9b2452de41 100644 --- a/x-pack/plugins/lens/server/index.ts +++ b/x-pack/plugins/lens/server/index.ts @@ -8,7 +8,9 @@ import { PluginInitializerContext, PluginConfigDescriptor } from 'kibana/server'; import { LensServerPlugin } from './plugin'; +export type { LensServerPluginSetup } from './plugin'; export * from './plugin'; +export * from './migrations/types'; import { configSchema, ConfigSchema } from '../config'; diff --git a/x-pack/plugins/lens/server/plugin.tsx b/x-pack/plugins/lens/server/plugin.tsx index f0ee801ece89b..e242fc8e4c5d6 100644 --- a/x-pack/plugins/lens/server/plugin.tsx +++ b/x-pack/plugins/lens/server/plugin.tsx @@ -36,7 +36,11 @@ export interface PluginStartContract { data: DataPluginStart; } -export class LensServerPlugin implements Plugin<{}, {}, {}, {}> { +export interface LensServerPluginSetup { + lensEmbeddableFactory: typeof lensEmbeddableFactory; +} + +export class LensServerPlugin implements Plugin { private readonly kibanaIndexConfig: Observable<{ kibana: { index: string } }>; private readonly telemetryLogger: Logger; @@ -63,8 +67,11 @@ export class LensServerPlugin implements Plugin<{}, {}, {}, {}> { plugins.taskManager ); } + plugins.embeddable.registerEmbeddableFactory(lensEmbeddableFactory()); - return {}; + return { + lensEmbeddableFactory, + }; } start(core: CoreStart, plugins: PluginStartContract) { diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/feature_importance/decision_path_chart.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/feature_importance/decision_path_chart.tsx index 7080d86498a51..dfaf58eba03d8 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/feature_importance/decision_path_chart.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/feature_importance/decision_path_chart.tsx @@ -43,7 +43,6 @@ const baselineStyle: LineAnnotationStyle = { details: { fontFamily: 'Arial', fontSize: 10, - fontStyle: 'bold', fill: euiColorMediumShade, padding: 0, }, diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/header/header.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/header/header.test.tsx index 8cd8977fcf741..62d828b337c2d 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/header/header.test.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/header/header.test.tsx @@ -52,7 +52,7 @@ describe('ExploratoryViewHeader', function () { to: 'now', }, }, - true + { openInNewTab: true } ); }); }); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/header/header.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/header/header.tsx index ded56ec9e817f..bfa457ee4025f 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/header/header.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/header/header.tsx @@ -69,7 +69,9 @@ export function ExploratoryViewHeader({ seriesId, lensAttributes }: Props) { timeRange, attributes: lensAttributes, }, - true + { + openInNewTab: true, + } ); } }} diff --git a/x-pack/plugins/observability/public/plugin.ts b/x-pack/plugins/observability/public/plugin.ts index f51f76a395199..7d11050f14d15 100644 --- a/x-pack/plugins/observability/public/plugin.ts +++ b/x-pack/plugins/observability/public/plugin.ts @@ -24,6 +24,7 @@ import type { DataPublicPluginSetup, DataPublicPluginStart, } from '../../../../src/plugins/data/public'; +import type { EmbeddableStart } from '../../../../src/plugins/embeddable/public'; import type { HomePublicPluginSetup, HomePublicPluginStart, @@ -52,6 +53,7 @@ export interface ObservabilityPublicPluginsSetup { export interface ObservabilityPublicPluginsStart { cases: CasesUiStart; + embeddable: EmbeddableStart; home?: HomePublicPluginStart; triggersActionsUi: TriggersAndActionsUIPublicPluginStart; data: DataPublicPluginStart; diff --git a/x-pack/plugins/osquery/public/scheduled_query_groups/scheduled_query_group_queries_table.tsx b/x-pack/plugins/osquery/public/scheduled_query_groups/scheduled_query_group_queries_table.tsx index ae3ddb1c0b861..1ab87949e3493 100644 --- a/x-pack/plugins/osquery/public/scheduled_query_groups/scheduled_query_group_queries_table.tsx +++ b/x-pack/plugins/osquery/public/scheduled_query_groups/scheduled_query_group_queries_table.tsx @@ -167,7 +167,7 @@ const ViewResultsInLensActionComponent: React.FC { - const openInNewWindow = !(!isModifiedEvent(event) && isLeftClickEvent(event)); + const openInNewTab = !(!isModifiedEvent(event) && isLeftClickEvent(event)); event.preventDefault(); @@ -181,7 +181,9 @@ const ViewResultsInLensActionComponent: React.FC> = [ name: '', }, { + className: 'flyoutOverviewDescription', field: 'description', truncateText: false, render: EnrichmentDescription, diff --git a/x-pack/plugins/security_solution/public/common/components/markdown_editor/editor.tsx b/x-pack/plugins/security_solution/public/common/components/markdown_editor/editor.tsx index f1fa6dc0fa1ef..c35d613203f76 100644 --- a/x-pack/plugins/security_solution/public/common/components/markdown_editor/editor.tsx +++ b/x-pack/plugins/security_solution/public/common/components/markdown_editor/editor.tsx @@ -5,8 +5,18 @@ * 2.0. */ -import React, { memo, useEffect, useState, useCallback } from 'react'; +import React, { + forwardRef, + memo, + useEffect, + useImperativeHandle, + useRef, + useState, + useCallback, + ElementRef, +} from 'react'; import { EuiMarkdownEditor } from '@elastic/eui'; +import { ContextShape } from '@elastic/eui/src/components/markdown_editor/markdown_context'; import { uiPlugins, parsingPlugins, processingPlugins } from './plugins'; @@ -20,41 +30,61 @@ interface MarkdownEditorProps { autoFocusDisabled?: boolean; } -const MarkdownEditorComponent: React.FC = ({ - onChange, - value, - ariaLabel, - editorId, - dataTestSubj, - height, - autoFocusDisabled = false, -}) => { - const [markdownErrorMessages, setMarkdownErrorMessages] = useState([]); - const onParse = useCallback((err, { messages }) => { - setMarkdownErrorMessages(err ? [err] : messages); - }, []); - - useEffect(() => { - if (!autoFocusDisabled) { - document.querySelector('textarea.euiMarkdownEditorTextArea')?.focus(); - } - }, [autoFocusDisabled]); - - return ( - - ); -}; +type EuiMarkdownEditorRef = ElementRef; + +export interface MarkdownEditorRef { + textarea: HTMLTextAreaElement | null; + replaceNode: ContextShape['replaceNode']; + toolbar: HTMLDivElement | null; +} + +const MarkdownEditorComponent = forwardRef( + ({ onChange, value, ariaLabel, editorId, dataTestSubj, height, autoFocusDisabled }, ref) => { + const [markdownErrorMessages, setMarkdownErrorMessages] = useState([]); + const onParse = useCallback((err, { messages }) => { + setMarkdownErrorMessages(err ? [err] : messages); + }, []); + const editorRef = useRef(null); + + useEffect(() => { + if (!autoFocusDisabled) { + editorRef.current?.textarea?.focus(); + } + }, [autoFocusDisabled]); + + // @ts-expect-error update types + useImperativeHandle(ref, () => { + if (!editorRef.current) { + return null; + } + + const editorNode = editorRef.current?.textarea?.closest('.euiMarkdownEditor'); + + return { + ...editorRef.current, + toolbar: editorNode?.querySelector('.euiMarkdownEditorToolbar'), + }; + }); + + return ( + + ); + } +); + +MarkdownEditorComponent.displayName = 'MarkdownEditorComponent'; export const MarkdownEditor = memo(MarkdownEditorComponent); diff --git a/x-pack/plugins/security_solution/public/common/components/markdown_editor/eui_form.tsx b/x-pack/plugins/security_solution/public/common/components/markdown_editor/eui_form.tsx index 1c407b3b8f8c2..82e4d5d5a2600 100644 --- a/x-pack/plugins/security_solution/public/common/components/markdown_editor/eui_form.tsx +++ b/x-pack/plugins/security_solution/public/common/components/markdown_editor/eui_form.tsx @@ -5,12 +5,12 @@ * 2.0. */ -import React from 'react'; +import React, { forwardRef } from 'react'; import styled from 'styled-components'; import { EuiMarkdownEditorProps, EuiFormRow, EuiFlexItem, EuiFlexGroup } from '@elastic/eui'; import { FieldHook, getFieldValidityAndErrorMessage } from '../../../shared_imports'; -import { MarkdownEditor } from './editor'; +import { MarkdownEditor, MarkdownEditorRef } from './editor'; type MarkdownEditorFormProps = EuiMarkdownEditorProps & { id: string; @@ -27,40 +27,41 @@ const BottomContentWrapper = styled(EuiFlexGroup)` `} `; -export const MarkdownEditorForm: React.FC = ({ - id, - field, - dataTestSubj, - idAria, - bottomRightContent, -}) => { - const { isInvalid, errorMessage } = getFieldValidityAndErrorMessage(field); +export const MarkdownEditorForm = React.memo( + forwardRef( + ({ id, field, dataTestSubj, idAria, bottomRightContent }, ref) => { + const { isInvalid, errorMessage } = getFieldValidityAndErrorMessage(field); - return ( - - <> - - {bottomRightContent && ( - - {bottomRightContent} - - )} - - - ); -}; + return ( + + <> + + {bottomRightContent && ( + + {bottomRightContent} + + )} + + + ); + } + ) +); + +MarkdownEditorForm.displayName = 'MarkdownEditorForm'; diff --git a/x-pack/plugins/security_solution/public/types.ts b/x-pack/plugins/security_solution/public/types.ts index 326a6973db53b..968211a0c82df 100644 --- a/x-pack/plugins/security_solution/public/types.ts +++ b/x-pack/plugins/security_solution/public/types.ts @@ -9,6 +9,7 @@ import { CoreStart } from '../../../../src/core/public'; import { HomePublicPluginSetup } from '../../../../src/plugins/home/public'; import { DataPublicPluginStart } from '../../../../src/plugins/data/public'; import { EmbeddableStart } from '../../../../src/plugins/embeddable/public'; +import { LensPublicStart } from '../../../plugins/lens/public'; import { NewsfeedPublicPluginStart } from '../../../../src/plugins/newsfeed/public'; import { Start as InspectorStart } from '../../../../src/plugins/inspector/public'; import { UiActionsStart } from '../../../../src/plugins/ui_actions/public'; @@ -58,6 +59,7 @@ export interface StartPlugins { embeddable: EmbeddableStart; inspector: InspectorStart; fleet?: FleetStart; + lens: LensPublicStart; lists?: ListsPluginStart; licensing: LicensingPluginStart; newsfeed?: NewsfeedPublicPluginStart; diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/advanced_pivot_editor/advanced_pivot_editor.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/advanced_pivot_editor/advanced_pivot_editor.tsx index eddfca5ecae29..4a9e8bcfc6b8d 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/advanced_pivot_editor/advanced_pivot_editor.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/advanced_pivot_editor/advanced_pivot_editor.tsx @@ -8,10 +8,12 @@ import { isEqual } from 'lodash'; import React, { memo, FC } from 'react'; -import { EuiCodeEditor, EuiFormRow } from '@elastic/eui'; +import { EuiFormRow } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import { CodeEditor } from '../../../../../../../../../src/plugins/kibana_react/public'; + import { StepDefineFormHook } from '../step_define'; export const AdvancedPivotEditor: FC = memo( @@ -25,14 +27,11 @@ export const AdvancedPivotEditor: FC label={i18n.translate('xpack.transform.stepDefineForm.advancedEditorLabel', { defaultMessage: 'Pivot configuration object', })} + data-test-subj="transformAdvancedPivotEditor" > - { setAdvancedEditorConfig(d); @@ -51,13 +50,21 @@ export const AdvancedPivotEditor: FC setAdvancedPivotEditorApplyButtonEnabled(false); } }} - setOptions={{ - fontSize: '12px', + options={{ + ariaLabel: i18n.translate('xpack.transform.stepDefineForm.advancedEditorAriaLabel', { + defaultMessage: 'Advanced pivot editor', + }), + automaticLayout: true, + fontSize: 12, + scrollBeyondLastLine: false, + quickSuggestions: true, + minimap: { + enabled: false, + }, + wordWrap: 'on', + wrappingIndent: 'indent', }} - theme="textmate" - aria-label={i18n.translate('xpack.transform.stepDefineForm.advancedEditorAriaLabel', { - defaultMessage: 'Advanced pivot editor', - })} + value={advancedEditorConfig} /> ); diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/advanced_runtime_mappings_editor/advanced_runtime_mappings_editor.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/advanced_runtime_mappings_editor/advanced_runtime_mappings_editor.tsx index 1e6e6a971a81a..cad258d192061 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/advanced_runtime_mappings_editor/advanced_runtime_mappings_editor.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/advanced_runtime_mappings_editor/advanced_runtime_mappings_editor.tsx @@ -8,10 +8,10 @@ import { isEqual } from 'lodash'; import React, { memo, FC } from 'react'; -import { EuiCodeEditor } from '@elastic/eui'; - import { i18n } from '@kbn/i18n'; +import { CodeEditor } from '../../../../../../../../../src/plugins/kibana_react/public'; + import { isRuntimeMappings } from '../../../../../../common/shared_imports'; import { StepDefineFormHook } from '../step_define'; @@ -26,42 +26,50 @@ export const AdvancedRuntimeMappingsEditor: FC { return ( - { - setAdvancedRuntimeMappingsConfig(d); +
+ { + setAdvancedRuntimeMappingsConfig(d); - // Disable the "Apply"-Button if the config hasn't changed. - if (advancedEditorRuntimeMappingsLastApplied === d) { - setRuntimeMappingsEditorApplyButtonEnabled(false); - return; - } + // Disable the "Apply"-Button if the config hasn't changed. + if (advancedEditorRuntimeMappingsLastApplied === d) { + setRuntimeMappingsEditorApplyButtonEnabled(false); + return; + } - // Try to parse the string passed on from the editor. - // If parsing fails, the "Apply"-Button will be disabled - try { - // if the user deletes the json in the editor - // they should still be able to apply changes - const isEmptyStr = d === ''; - const parsedJson = isEmptyStr ? {} : JSON.parse(convertToJson(d)); - setRuntimeMappingsEditorApplyButtonEnabled(isEmptyStr || isRuntimeMappings(parsedJson)); - } catch (e) { - setRuntimeMappingsEditorApplyButtonEnabled(false); - } - }} - setOptions={{ - fontSize: '12px', - }} - theme="textmate" - aria-label={i18n.translate('xpack.transform.stepDefineForm.advancedEditorAriaLabel', { - defaultMessage: 'Advanced pivot editor', - })} - /> + // Try to parse the string passed on from the editor. + // If parsing fails, the "Apply"-Button will be disabled + try { + // if the user deletes the json in the editor + // they should still be able to apply changes + const isEmptyStr = d === ''; + const parsedJson = isEmptyStr ? {} : JSON.parse(convertToJson(d)); + setRuntimeMappingsEditorApplyButtonEnabled( + isEmptyStr || isRuntimeMappings(parsedJson) + ); + } catch (e) { + setRuntimeMappingsEditorApplyButtonEnabled(false); + } + }} + options={{ + ariaLabel: i18n.translate('xpack.transform.stepDefineForm.advancedEditorAriaLabel', { + defaultMessage: 'Advanced pivot editor', + }), + automaticLayout: true, + fontSize: 12, + scrollBeyondLastLine: false, + quickSuggestions: true, + minimap: { + enabled: false, + }, + wordWrap: 'on', + wrappingIndent: 'indent', + }} + value={advancedRuntimeMappingsConfig} + /> +
); }, (prevProps, nextProps) => isEqual(pickProps(prevProps), pickProps(nextProps)) diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/advanced_source_editor/advanced_source_editor.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/advanced_source_editor/advanced_source_editor.tsx index 1c7c58be48be6..b711a5a0cbb81 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/advanced_source_editor/advanced_source_editor.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/advanced_source_editor/advanced_source_editor.tsx @@ -7,10 +7,10 @@ import React, { FC } from 'react'; -import { EuiCodeEditor } from '@elastic/eui'; - import { i18n } from '@kbn/i18n'; +import { CodeEditor } from '../../../../../../../../../src/plugins/kibana_react/public'; + import { StepDefineFormHook } from '../step_define'; export const AdvancedSourceEditor: FC = ({ @@ -23,38 +23,48 @@ export const AdvancedSourceEditor: FC = ({ }, }) => { return ( - { - setSearchString(undefined); - setAdvancedEditorSourceConfig(d); +
+ { + setSearchString(undefined); + setAdvancedEditorSourceConfig(d); - // Disable the "Apply"-Button if the config hasn't changed. - if (advancedEditorSourceConfigLastApplied === d) { - setAdvancedSourceEditorApplyButtonEnabled(false); - return; - } + // Disable the "Apply"-Button if the config hasn't changed. + if (advancedEditorSourceConfigLastApplied === d) { + setAdvancedSourceEditorApplyButtonEnabled(false); + return; + } - // Try to parse the string passed on from the editor. - // If parsing fails, the "Apply"-Button will be disabled - try { - JSON.parse(d); - setAdvancedSourceEditorApplyButtonEnabled(true); - } catch (e) { - setAdvancedSourceEditorApplyButtonEnabled(false); - } - }} - setOptions={{ - fontSize: '12px', - }} - theme="textmate" - aria-label={i18n.translate('xpack.transform.stepDefineForm.advancedSourceEditorAriaLabel', { - defaultMessage: 'Advanced query editor', - })} - /> + // Try to parse the string passed on from the editor. + // If parsing fails, the "Apply"-Button will be disabled + try { + JSON.parse(d); + setAdvancedSourceEditorApplyButtonEnabled(true); + } catch (e) { + setAdvancedSourceEditorApplyButtonEnabled(false); + } + }} + options={{ + ariaLabel: i18n.translate( + 'xpack.transform.stepDefineForm.advancedSourceEditorAriaLabel', + { + defaultMessage: 'Advanced query editor', + } + ), + automaticLayout: true, + fontSize: 12, + scrollBeyondLastLine: false, + quickSuggestions: true, + minimap: { + enabled: false, + }, + wordWrap: 'on', + wrappingIndent: 'indent', + }} + value={advancedEditorSourceConfig} + /> +
); }; diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/aggregation_list/popover_form.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/aggregation_list/popover_form.tsx index 831ee17371910..53f2716551289 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/aggregation_list/popover_form.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/aggregation_list/popover_form.tsx @@ -11,7 +11,7 @@ import { i18n } from '@kbn/i18n'; import { EuiButton, - EuiCodeEditor, + EuiCodeBlock, EuiComboBox, EuiFieldText, EuiForm, @@ -326,16 +326,17 @@ export const PopoverForm: React.FC = ({ defaultData, otherAggNames, onCha )} {isUnsupportedAgg && ( - + + {JSON.stringify(getEsAggFromAggConfig(defaultData), null, 2)} + )} = ({ defaultData, otherAggNames, onCha {isUnsupportedAgg && ( <> - + + {JSON.stringify(getEsAggFromGroupByConfig(defaultData), null, 2)} + )} diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/filter_agg/components/editor_form.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/filter_agg/components/editor_form.tsx index edb43afdd90d4..8a8c12dfc5583 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/filter_agg/components/editor_form.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/filter_agg/components/editor_form.tsx @@ -6,7 +6,8 @@ */ import React from 'react'; -import { EuiCodeEditor, EuiSpacer } from '@elastic/eui'; +import { EuiSpacer } from '@elastic/eui'; +import { CodeEditor } from '../../../../../../../../../../../../src/plugins/kibana_react/public'; import { FilterAggConfigEditor } from '../types'; export const FilterEditorForm: FilterAggConfigEditor['aggTypeConfig']['FilterAggFormComponent'] = ({ @@ -16,15 +17,24 @@ export const FilterEditorForm: FilterAggConfigEditor['aggTypeConfig']['FilterAgg return ( <> - { onChange({ config: d }); }} - mode="json" - style={{ width: '100%' }} - theme="textmate" - height="300px" + options={{ + automaticLayout: true, + fontSize: 12, + scrollBeyondLastLine: false, + quickSuggestions: true, + minimap: { + enabled: false, + }, + wordWrap: 'on', + wrappingIndent: 'indent', + }} + value={config || ''} /> ); diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/__snapshots__/expanded_row_json_pane.test.tsx.snap b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/__snapshots__/expanded_row_json_pane.test.tsx.snap index dea6f57bcaab0..7a640389e2915 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/__snapshots__/expanded_row_json_pane.test.tsx.snap +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/__snapshots__/expanded_row_json_pane.test.tsx.snap @@ -9,50 +9,53 @@ exports[`Transform: Transform List Expanded Row Minimal - + value= + { + "id": "fq_date_histogram_1m_1441", + "source": { + "index": [ + "farequote-2019" ], - \\"query\\": { - \\"match_all\\": {} + "query": { + "match_all": {} } }, - \\"dest\\": { - \\"index\\": \\"fq_date_histogram_1m_1441\\" + "dest": { + "index": "fq_date_histogram_1m_1441" }, - \\"pivot\\": { - \\"group_by\\": { - \\"@timestamp\\": { - \\"date_histogram\\": { - \\"field\\": \\"@timestamp\\", - \\"calendar_interval\\": \\"1m\\" + "pivot": { + "group_by": { + "@timestamp": { + "date_histogram": { + "field": "@timestamp", + "calendar_interval": "1m" } } }, - \\"aggregations\\": { - \\"responsetime.avg\\": { - \\"avg\\": { - \\"field\\": \\"responsetime\\" + "aggregations": { + "responsetime.avg": { + "avg": { + "field": "responsetime" } } } }, - \\"version\\": \\"8.0.0\\", - \\"create_time\\": 1564388146667 -}" - /> + "version": "8.0.0", + "create_time": 1564388146667 +} + = ({ json }) => { - + isCopyable + > + value={JSON.stringify(json, null, 2)} +   diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 40a26a7895789..050818c5ba4ef 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -9814,13 +9814,11 @@ "xpack.fleet.overviewPageTitle": "Fleet", "xpack.fleet.packagePolicyInputOverrideError": "パッケージ{packageName}には入力タイプ{inputType}が存在しません。", "xpack.fleet.packagePolicyStreamOverrideError": "パッケージ{packageName}の{inputType}にはデータストリーム{streamSet}が存在しません", - "xpack.fleet.packagePolicyStreamVarOverrideError": "パッケージ{packageName}の{inputType}の{streamSet}にはVar {varName}が存在しません", "xpack.fleet.packagePolicyValidation.invalidArrayErrorMessage": "無効なフォーマット", "xpack.fleet.packagePolicyValidation.invalidYamlFormatErrorMessage": "YAML形式が無効です", "xpack.fleet.packagePolicyValidation.nameRequiredErrorMessage": "名前が必要です", "xpack.fleet.packagePolicyValidation.quoteStringErrorMessage": "*や&などの特殊YAML文字で始まる文字列は二重引用符で囲む必要があります。", "xpack.fleet.packagePolicyValidation.requiredErrorMessage": "{fieldName}が必要です", - "xpack.fleet.packagePolicyVarOverrideError": "パッケージ{packageName}の{inputType}にはVar {varName}が存在しません", "xpack.fleet.permissionDeniedErrorMessage": "Fleet へのアクセスが許可されていません。Fleet には{roleName}権限が必要です。", "xpack.fleet.permissionDeniedErrorTitle": "パーミッションが拒否されました", "xpack.fleet.permissionsRequestErrorMessageDescription": "Fleet アクセス権の確認中に問題が発生しました", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 172685f337150..119ed34594343 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -10075,13 +10075,11 @@ "xpack.fleet.overviewPageTitle": "Fleet", "xpack.fleet.packagePolicyInputOverrideError": "输入类型 {inputType} 在软件包 {packageName} 上不存在", "xpack.fleet.packagePolicyStreamOverrideError": "数据流 {streamSet} 在软件包 {packageName} 的 {inputType} 上不存在", - "xpack.fleet.packagePolicyStreamVarOverrideError": "变量 {varName} 在软件包 {packageName} 的 {inputType} 的 {streamSet} 上不存在", "xpack.fleet.packagePolicyValidation.invalidArrayErrorMessage": "格式无效", "xpack.fleet.packagePolicyValidation.invalidYamlFormatErrorMessage": "YAML 格式无效", "xpack.fleet.packagePolicyValidation.nameRequiredErrorMessage": "“名称”必填", "xpack.fleet.packagePolicyValidation.quoteStringErrorMessage": "以特殊 YAML 字符(* 或 &)开头的字符串需要使用双引号引起。", "xpack.fleet.packagePolicyValidation.requiredErrorMessage": "“{fieldName}”必填", - "xpack.fleet.packagePolicyVarOverrideError": "变量 {varName} 在软件包 {packageName} 的 {inputType} 上不存在", "xpack.fleet.permissionDeniedErrorMessage": "您无权访问 Fleet。Fleet 需要 {roleName} 权限。", "xpack.fleet.permissionDeniedErrorTitle": "权限被拒绝", "xpack.fleet.permissionsRequestErrorMessageDescription": "检查 Fleet 权限时遇到问题", diff --git a/x-pack/plugins/uptime/public/components/common/charts/__snapshots__/donut_chart.test.tsx.snap b/x-pack/plugins/uptime/public/components/common/charts/__snapshots__/donut_chart.test.tsx.snap index 1b7be5dc261b8..e6e235f75c557 100644 --- a/x-pack/plugins/uptime/public/components/common/charts/__snapshots__/donut_chart.test.tsx.snap +++ b/x-pack/plugins/uptime/public/components/common/charts/__snapshots__/donut_chart.test.tsx.snap @@ -53,7 +53,6 @@ exports[`DonutChart component passes correct props without errors for valid prop "fill": "#333", "fontFamily": "sans-serif", "fontSize": 10, - "fontStyle": "bold", "padding": Object { "inner": 8, "outer": 0, @@ -64,7 +63,6 @@ exports[`DonutChart component passes correct props without errors for valid prop "fill": "#333", "fontFamily": "sans-serif", "fontSize": 12, - "fontStyle": "bold", "padding": Object { "inner": 8, "outer": 0, diff --git a/x-pack/test/apm_api_integration/tests/feature_controls.ts b/x-pack/test/apm_api_integration/tests/feature_controls.ts index 11c16fd87483c..58193726e20f1 100644 --- a/x-pack/test/apm_api_integration/tests/feature_controls.ts +++ b/x-pack/test/apm_api_integration/tests/feature_controls.ts @@ -86,7 +86,7 @@ export default function featureControlsTests({ getService }: FtrProviderContext) }, { req: { - url: `/api/apm/services/foo/agent_name?start=${start}&end=${end}`, + url: `/api/apm/services/foo/agent?start=${start}&end=${end}`, }, expectForbidden: expect403, expectResponse: expect200, diff --git a/x-pack/test/apm_api_integration/tests/index.ts b/x-pack/test/apm_api_integration/tests/index.ts index 0c1f695d4395b..0e76a4ed86688 100644 --- a/x-pack/test/apm_api_integration/tests/index.ts +++ b/x-pack/test/apm_api_integration/tests/index.ts @@ -82,8 +82,8 @@ export default function apmApiIntegrationTests(providerContext: FtrProviderConte }); // Services - describe('services/agent_name', function () { - loadTestFile(require.resolve('./services/agent_name')); + describe('services/agent', function () { + loadTestFile(require.resolve('./services/agent')); }); describe('services/annotations', function () { diff --git a/x-pack/test/apm_api_integration/tests/services/agent_name.ts b/x-pack/test/apm_api_integration/tests/services/agent.ts similarity index 85% rename from x-pack/test/apm_api_integration/tests/services/agent_name.ts rename to x-pack/test/apm_api_integration/tests/services/agent.ts index 258146dc30be1..5fd222c72a3b2 100644 --- a/x-pack/test/apm_api_integration/tests/services/agent_name.ts +++ b/x-pack/test/apm_api_integration/tests/services/agent.ts @@ -21,7 +21,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { registry.when('Agent name when data is not loaded', { config: 'basic', archives: [] }, () => { it('handles the empty state', async () => { const response = await supertest.get( - `/api/apm/services/opbeans-node/agent_name?start=${start}&end=${end}` + `/api/apm/services/opbeans-node/agent?start=${start}&end=${end}` ); expect(response.status).to.be(200); @@ -35,12 +35,12 @@ export default function ApiTest({ getService }: FtrProviderContext) { () => { it('returns the agent name', async () => { const response = await supertest.get( - `/api/apm/services/opbeans-node/agent_name?start=${start}&end=${end}` + `/api/apm/services/opbeans-node/agent?start=${start}&end=${end}` ); expect(response.status).to.be(200); - expect(response.body).to.eql({ agentName: 'nodejs' }); + expect(response.body).to.eql({ agentName: 'nodejs', runtimeName: 'node' }); }); } ); 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 b310c88b5854d..ad924003d10f3 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 @@ -77,7 +77,23 @@ export default function (providerContext: FtrProviderContext) { policy_id: agentPolicyId, enabled: true, output_id: '', - inputs: [], + 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', @@ -226,7 +242,7 @@ export default function (providerContext: FtrProviderContext) { }); describe('when "dryRun: true" is provided', function () { - it('should return a diff with missingVars', async function () { + it('should return a diff with no errors', async function () { const { body }: { body: UpgradePackagePolicyDryRunResponse } = await supertest .post(`/api/fleet/package_policies/upgrade`) .set('kbn-xsrf', 'xxxx') @@ -239,7 +255,6 @@ export default function (providerContext: FtrProviderContext) { expect(body.length).to.be(1); expect(body[0].diff?.length).to.be(2); expect(body[0].hasErrors).to.be(false); - expect(body[0].diff?.[1].missingVars).to.contain('test_var'); }); }); diff --git a/x-pack/test/fleet_functional/apps/home/welcome.ts b/x-pack/test/fleet_functional/apps/home/welcome.ts index 3a9a3a05e9226..678ee10e9b83b 100644 --- a/x-pack/test/fleet_functional/apps/home/welcome.ts +++ b/x-pack/test/fleet_functional/apps/home/welcome.ts @@ -14,7 +14,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['common', 'home']); const kibanaServer = getService('kibanaServer'); - describe('Welcome interstitial', () => { + // flaky https://github.com/elastic/kibana/issues/109017 + describe.skip('Welcome interstitial', () => { before(async () => { // Need to navigate to page first to clear storage before test can be run await PageObjects.common.navigateToUrl('home', undefined); diff --git a/x-pack/test/functional/apps/lens/chart_data.ts b/x-pack/test/functional/apps/lens/chart_data.ts index 87147f3d8ae05..96482b56fdbe1 100644 --- a/x-pack/test/functional/apps/lens/chart_data.ts +++ b/x-pack/test/functional/apps/lens/chart_data.ts @@ -125,11 +125,12 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { // assert legend expect(debugState.legend!.items).to.eql([ - { key: '5722.77', name: '> 5,722.77', color: '#6092c0' }, - { key: '8529.22', name: '> 8,529.22', color: '#a8bfda' }, - { key: '11335.66', name: '> 11,335.66', color: '#ebeff5' }, - { key: '14142.11', name: '> 14,142.11', color: '#ecb385' }, - { key: '16948.55', name: '> 16,948.55', color: '#e7664c' }, + { color: '#6092c0', key: '≥ 5,722.775', name: '≥ 5,722.775' }, + { color: '#6092c0', key: '≥ 5,722.77', name: '≥ 5,722.77' }, + { color: '#a8bfda', key: '≥ 8,529.22', name: '≥ 8,529.22' }, + { color: '#ebeff5', key: '≥ 11,335.66', name: '≥ 11,335.66' }, + { color: '#ecb385', key: '≥ 14,142.11', name: '≥ 14,142.11' }, + { color: '#e7664c', key: '≥ 16,948.55', name: '≥ 16,948.55' }, ]); }); diff --git a/x-pack/test/functional/apps/lens/heatmap.ts b/x-pack/test/functional/apps/lens/heatmap.ts index 4004ee1124f0e..0972f4809def1 100644 --- a/x-pack/test/functional/apps/lens/heatmap.ts +++ b/x-pack/test/functional/apps/lens/heatmap.ts @@ -60,11 +60,12 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { // assert legend expect(debugState.legend!.items).to.eql([ - { key: '5722.77', name: '> 5,722.77', color: '#6092c0' }, - { key: '8529.22', name: '> 8,529.22', color: '#a8bfda' }, - { key: '11335.66', name: '> 11,335.66', color: '#ebeff5' }, - { key: '14142.11', name: '> 14,142.11', color: '#ecb385' }, - { key: '16948.55', name: '> 16,948.55', color: '#e7664c' }, + { key: '≥ 5,722.775', name: '≥ 5,722.775', color: '#6092c0' }, + { key: '≥ 5,722.77', name: '≥ 5,722.77', color: '#6092c0' }, + { key: '≥ 8,529.22', name: '≥ 8,529.22', color: '#a8bfda' }, + { key: '≥ 11,335.66', name: '≥ 11,335.66', color: '#ebeff5' }, + { key: '≥ 14,142.11', name: '≥ 14,142.11', color: '#ecb385' }, + { key: '≥ 16,948.55', name: '≥ 16,948.55', color: '#e7664c' }, ]); }); @@ -84,11 +85,12 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { // assert legend has changed expect(debugState.legend!.items).to.eql([ - { key: '7126', name: '> 7,126', color: '#6092c0' }, - { key: '8529.22', name: '> 8,529.22', color: '#a8bfda' }, - { key: '11335.66', name: '> 11,335.66', color: '#ebeff5' }, - { key: '14142.11', name: '> 14,142.11', color: '#ecb385' }, - { key: '16948.55', name: '> 16,948.55', color: '#e7664c' }, + { key: '≥ 5,722.775', name: '≥ 5,722.775', color: '#6092c0' }, + { key: '≥ 7,126', name: '≥ 7,126', color: '#6092c0' }, + { key: '≥ 8,529.22', name: '≥ 8,529.22', color: '#a8bfda' }, + { key: '≥ 11,335.66', name: '≥ 11,335.66', color: '#ebeff5' }, + { key: '≥ 14,142.11', name: '≥ 14,142.11', color: '#ecb385' }, + { key: '≥ 16,948.55', name: '≥ 16,948.55', color: '#e7664c' }, ]); }); @@ -104,11 +106,12 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { // assert legend has changed expect(debugState.legend!.items).to.eql([ - { key: '7126', name: '> 7,126', color: '#6092c0' }, - { key: '8529.22', name: '> 8,529.22', color: '#a8bfda' }, - { key: '11335.66', name: '> 11,335.66', color: '#ebeff5' }, - { key: '14142.11', name: '> 14,142.11', color: '#ecb385' }, - { key: '16948.55', name: '> 16,948.55', color: '#e7664c' }, + { key: '≥ 5,722.775', name: '≥ 5,722.775', color: '#6092c0' }, + { key: '≥ 7,126', name: '≥ 7,126', color: '#6092c0' }, + { key: '≥ 8,529.22', name: '≥ 8,529.22', color: '#a8bfda' }, + { key: '≥ 11,335.66', name: '≥ 11,335.66', color: '#ebeff5' }, + { key: '≥ 14,142.11', name: '≥ 14,142.11', color: '#ecb385' }, + { key: '≥ 16,948.55', name: '≥ 16,948.55', color: '#e7664c' }, ]); }); @@ -126,11 +129,12 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { // assert legend has changed expect(debugState.legend!.items).to.eql([ - { key: '0', name: '> 0', color: '#6092c0' }, - { key: '8529.22', name: '> 8,529.22', color: '#a8bfda' }, - { key: '11335.66', name: '> 11,335.66', color: '#ebeff5' }, - { key: '14142.11', name: '> 14,142.11', color: '#ecb385' }, - { key: '16948.55', name: '> 16,948.55', color: '#e7664c' }, + { key: '≥ 5,722.775', name: '≥ 5,722.775', color: '#6092c0' }, + { key: '≥ 0', name: '≥ 0', color: '#6092c0' }, + { key: '≥ 8,529.22', name: '≥ 8,529.22', color: '#a8bfda' }, + { key: '≥ 11,335.66', name: '≥ 11,335.66', color: '#ebeff5' }, + { key: '≥ 14,142.11', name: '≥ 14,142.11', color: '#ecb385' }, + { key: '≥ 16,948.55', name: '≥ 16,948.55', color: '#e7664c' }, ]); }); @@ -146,11 +150,12 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { // assert legend has changed expect(debugState.legend!.items).to.eql([ - { key: '5722.77', name: '> 5,722.77', color: '#209280' }, - { key: '8529.22', name: '> 8,529.22', color: '#54b399' }, - { key: '11335.66', name: '> 11,335.66', color: '#d6bf57' }, - { key: '14142.11', name: '> 14,142.11', color: '#e7664c' }, - { key: '16948.55', name: '> 16,948.55', color: '#cc5642' }, + { key: '≥ 5,722.775', name: '≥ 5,722.775', color: '#209280' }, + { key: '≥ 5,722.77', name: '≥ 5,722.77', color: '#209280' }, + { key: '≥ 8,529.22', name: '≥ 8,529.22', color: '#54b399' }, + { key: '≥ 11,335.66', name: '≥ 11,335.66', color: '#d6bf57' }, + { key: '≥ 14,142.11', name: '≥ 14,142.11', color: '#e7664c' }, + { key: '≥ 16,948.55', name: '≥ 16,948.55', color: '#cc5642' }, ]); }); @@ -166,11 +171,12 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { // assert legend has not changed expect(debugState.legend!.items).to.eql([ - { key: '5722.77', name: '> 5,722.77', color: '#209280' }, - { key: '8529.22', name: '> 8,529.22', color: '#54b399' }, - { key: '11335.66', name: '> 11,335.66', color: '#d6bf57' }, - { key: '14142.11', name: '> 14,142.11', color: '#e7664c' }, - { key: '16948.55', name: '> 16,948.55', color: '#cc5642' }, + { key: '≥ 5,722.775', name: '≥ 5,722.775', color: '#209280' }, + { key: '≥ 5,722.77', name: '≥ 5,722.77', color: '#209280' }, + { key: '≥ 8,529.22', name: '≥ 8,529.22', color: '#54b399' }, + { key: '≥ 11,335.66', name: '≥ 11,335.66', color: '#d6bf57' }, + { key: '≥ 14,142.11', name: '≥ 14,142.11', color: '#e7664c' }, + { key: '≥ 16,948.55', name: '≥ 16,948.55', color: '#cc5642' }, ]); }); }); diff --git a/x-pack/test/functional/services/transform/wizard.ts b/x-pack/test/functional/services/transform/wizard.ts index 4869237eb7db4..275002155d7e0 100644 --- a/x-pack/test/functional/services/transform/wizard.ts +++ b/x-pack/test/functional/services/transform/wizard.ts @@ -385,9 +385,9 @@ export function TransformWizardProvider({ getService, getPageObjects }: FtrProvi async assertRuntimeMappingsEditorContent(expectedContent: string[]) { await this.assertRuntimeMappingsEditorExists(); - const runtimeMappingsEditorString = await aceEditor.getValue( - 'transformAdvancedRuntimeMappingsEditor' - ); + const wrapper = await testSubjects.find('transformAdvancedRuntimeMappingsEditor'); + const editor = await wrapper.findByCssSelector('.monaco-editor .view-lines'); + const runtimeMappingsEditorString = await editor.getVisibleText(); // Not all lines may be visible in the editor and thus aceEditor may not return all lines. // This means we might not get back valid JSON so we only test against the first few lines // and see if the string matches. @@ -624,7 +624,9 @@ export function TransformWizardProvider({ getService, getPageObjects }: FtrProvi }, async assertAdvancedPivotEditorContent(expectedValue: string[]) { - const advancedEditorString = await aceEditor.getValue('transformAdvancedPivotEditor'); + const wrapper = await testSubjects.find('transformAdvancedPivotEditor'); + const editor = await wrapper.findByCssSelector('.monaco-editor .view-lines'); + const advancedEditorString = await editor.getVisibleText(); // Not all lines may be visible in the editor and thus aceEditor may not return all lines. // This means we might not get back valid JSON so we only test against the first few lines // and see if the string matches. diff --git a/yarn.lock b/yarn.lock index 747bb87610539..e49d7590c21aa 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1389,10 +1389,10 @@ dependencies: object-hash "^1.3.0" -"@elastic/charts@33.2.2": - version "33.2.2" - resolved "https://registry.yarnpkg.com/@elastic/charts/-/charts-33.2.2.tgz#403c46eebe71f4ca7e5c9c1a135eec66869961cc" - integrity sha512-g+z1T8s6m7eySaxcY7R6yqUHUstUtEIH0P4FineKWdZ5L6IkxBNrhM7r0FaddIurNxvBy/SGQorhmFZAksWhiQ== +"@elastic/charts@34.0.0": + version "34.0.0" + resolved "https://registry.yarnpkg.com/@elastic/charts/-/charts-34.0.0.tgz#42288a6b3a303ccc61385b786f2ccf3549c3b43a" + integrity sha512-gXekMH6iWIo5DaUzPJLjbn02CuPaxwGIOOF2cz/UH9zRY2A5UZ8CDICysDgriK1PcJfKPCa7Yk5cntn590coyg== dependencies: "@popperjs/core" "^2.4.0" chroma-js "^2.1.0"