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..63cbe52b55030 100644
--- a/package.json
+++ b/package.json
@@ -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/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/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/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/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 = ({
- 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;