[Cases] Fixing newline issue with markdown stringify (#111646)

* Fixing newline issue with markdown stringify

* Adding comments and removing null check

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Jonathan Buttner 2021-09-13 12:54:17 -04:00 committed by GitHub
parent 1e70ad1dcf
commit 5e15e2ff1c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 480 additions and 357 deletions

View file

@ -0,0 +1,22 @@
/*
* 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 { parseCommentString, stringifyMarkdownComment } from './utils';
describe('markdown utils', () => {
describe('stringifyComment', () => {
it('adds a newline to the end if one does not exist', () => {
const parsed = parseCommentString('hello');
expect(stringifyMarkdownComment(parsed)).toEqual('hello\n');
});
it('does not add a newline to the end if one already exists', () => {
const parsed = parseCommentString('hello\n');
expect(stringifyMarkdownComment(parsed)).toEqual('hello\n');
});
});
});

View file

@ -16,26 +16,33 @@ import { SerializableRecord } from '@kbn/utility-types';
import { LENS_ID, LensParser, LensSerializer } from './lens';
import { TimelineSerializer, TimelineParser } from './timeline';
interface LensMarkdownNode extends Node {
export interface LensMarkdownNode extends Node {
timeRange: TimeRange;
attributes: SerializableRecord;
type: string;
id: string;
}
interface LensMarkdownParent extends Node {
/**
* A node that has children of other nodes describing the markdown elements or a specific lens visualization.
*/
export interface MarkdownNode extends Node {
children: Array<LensMarkdownNode | Node>;
}
export const getLensVisualizations = (parsedComment?: Array<LensMarkdownNode | Node>) =>
(parsedComment?.length ? filter(parsedComment, { type: LENS_ID }) : []) as LensMarkdownNode[];
/**
* Converts a text comment into a series of markdown nodes that represent a lens visualization, a timeline link, or just
* plain markdown.
*/
export const parseCommentString = (comment: string) => {
const processor = unified().use([[markdown, {}], LensParser, TimelineParser]);
return processor.parse(comment) as LensMarkdownParent;
return processor.parse(comment) as MarkdownNode;
};
export const stringifyComment = (comment: LensMarkdownParent) =>
export const stringifyMarkdownComment = (comment: MarkdownNode) =>
unified()
.use([
[
@ -54,3 +61,13 @@ export const stringifyComment = (comment: LensMarkdownParent) =>
],
])
.stringify(comment);
export const isLensMarkdownNode = (node?: unknown): node is LensMarkdownNode => {
const unsafeNode = node as LensMarkdownNode;
return (
unsafeNode != null &&
unsafeNode.timeRange != null &&
unsafeNode.attributes != null &&
unsafeNode.type === 'lens'
);
};

View file

@ -0,0 +1,283 @@
/*
* 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, stringifyCommentWithoutTrailingNewline } from './comments';
import {
getLensVisualizations,
parseCommentString,
} from '../../../common/utils/markdown_plugins/utils';
import { savedObjectsServiceMock } from '../../../../../../src/core/server/mocks';
import { lensEmbeddableFactory } from '../../../../lens/server/embeddable/lens_embeddable_factory';
import { LensDocShape715 } from '../../../../lens/server';
import { SavedObjectReference } from 'kibana/server';
const migrations = createCommentsMigrations({
lensEmbeddableFactory,
});
const contextMock = savedObjectsServiceMock.createMigrationContext();
describe('index migrations', () => {
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
) as unknown) as Array<{
attributes: LensDocShape715 & { references: SavedObjectReference[] };
}>;
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' });
});
});
describe('stringifyCommentWithoutTrailingNewline', () => {
it('removes the newline added by the markdown library when the comment did not originally have one', () => {
const originalComment = 'awesome';
const parsedString = parseCommentString(originalComment);
expect(stringifyCommentWithoutTrailingNewline(originalComment, parsedString)).toEqual(
'awesome'
);
});
it('leaves the newline if it was in the original comment', () => {
const originalComment = 'awesome\n';
const parsedString = parseCommentString(originalComment);
expect(stringifyCommentWithoutTrailingNewline(originalComment, parsedString)).toEqual(
'awesome\n'
);
});
it('does not remove newlines that are not at the end of the comment', () => {
const originalComment = 'awesome\ncomment';
const parsedString = parseCommentString(originalComment);
expect(stringifyCommentWithoutTrailingNewline(originalComment, parsedString)).toEqual(
'awesome\ncomment'
);
});
it('does not remove spaces at the end of the comment', () => {
const originalComment = 'awesome ';
const parsedString = parseCommentString(originalComment);
expect(stringifyCommentWithoutTrailingNewline(originalComment, parsedString)).toEqual(
'awesome '
);
});
});
});
});

View file

@ -0,0 +1,152 @@
/*
* 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 { mapValues, trimEnd } from 'lodash';
import { SerializableRecord } from '@kbn/utility-types';
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 { CommentType, AssociationType } from '../../../common';
import {
isLensMarkdownNode,
LensMarkdownNode,
MarkdownNode,
parseCommentString,
stringifyMarkdownComment,
} from '../../../common/utils/markdown_plugins/utils';
import { addOwnerToSO, SanitizedCaseOwner } from '.';
interface UnsanitizedComment {
comment: string;
type?: CommentType;
}
interface SanitizedComment {
comment: string;
type: CommentType;
}
interface SanitizedCommentForSubCases {
associationType: AssociationType;
rule?: { id: string | null; name: string | null };
}
export interface CreateCommentsMigrationsDeps {
lensEmbeddableFactory: LensServerPluginSetup['lensEmbeddableFactory'];
}
export const createCommentsMigrations = (
migrationDeps: CreateCommentsMigrationsDeps
): SavedObjectMigrationMap => {
const embeddableMigrations = mapValues<
MigrateFunctionsObject,
SavedObjectMigrationFn<{ comment?: string }>
>(
migrationDeps.lensEmbeddableFactory().migrations,
migrateByValueLensVisualizations
) as MigrateFunctionsObject;
const commentsMigrations = {
'7.11.0': (
doc: SavedObjectUnsanitizedDoc<UnsanitizedComment>
): SavedObjectSanitizedDoc<SanitizedComment> => {
return {
...doc,
attributes: {
...doc.attributes,
type: CommentType.user,
},
references: doc.references || [],
};
},
'7.12.0': (
doc: SavedObjectUnsanitizedDoc<UnsanitizedComment>
): SavedObjectSanitizedDoc<SanitizedCommentForSubCases> => {
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': (
doc: SavedObjectUnsanitizedDoc<Record<string, unknown>>
): SavedObjectSanitizedDoc<SanitizedCaseOwner> => {
return addOwnerToSO(doc);
},
};
return mergeMigrationFunctionMaps(commentsMigrations, embeddableMigrations);
};
const migrateByValueLensVisualizations = (
migrate: MigrateFunction,
version: string
): SavedObjectMigrationFn<{ comment?: string }> => (
doc: SavedObjectUnsanitizedDoc<{ comment?: string }>
) => {
if (doc.attributes.comment == null) {
return doc;
}
const parsedComment = parseCommentString(doc.attributes.comment);
const migratedComment = parsedComment.children.map((comment) => {
if (isLensMarkdownNode(comment)) {
// casting here because ts complains that comment isn't serializable because LensMarkdownNode
// extends Node which has fields that conflict with SerializableRecord even though it is serializable
return migrate(comment as SerializableRecord) as LensMarkdownNode;
}
return comment;
});
const migratedMarkdown = { ...parsedComment, children: migratedComment };
return {
...doc,
attributes: {
...doc.attributes,
comment: stringifyCommentWithoutTrailingNewline(doc.attributes.comment, migratedMarkdown),
},
};
};
export const stringifyCommentWithoutTrailingNewline = (
originalComment: string,
markdownNode: MarkdownNode
) => {
const stringifiedComment = stringifyMarkdownComment(markdownNode);
// if the original comment already ended with a newline then just leave it there
if (originalComment.endsWith('\n')) {
return stringifiedComment;
}
// the original comment did not end with a newline so the markdown library is going to add one, so let's remove it
// so the comment stays consistent
return trimEnd(stringifiedComment, '\n');
};

View file

@ -1,245 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { createCommentsMigrations } from './index';
import {
getLensVisualizations,
parseCommentString,
} from '../../../common/utils/markdown_plugins/utils';
import { savedObjectsServiceMock } from '../../../../../../src/core/server/mocks';
import { lensEmbeddableFactory } from '../../../../lens/server/embeddable/lens_embeddable_factory';
import { LensDocShape715 } from '../../../../lens/server';
import { SavedObjectReference } from 'kibana/server';
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
) as unknown) as Array<{
attributes: LensDocShape715 & { references: SavedObjectReference[] };
}>;
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' });
});
});
});

View file

@ -7,30 +7,15 @@
/* eslint-disable @typescript-eslint/naming-convention */
import { 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,
CommentType,
AssociationType,
SECURITY_SOLUTION_OWNER,
} from '../../../common';
import { parseCommentString, stringifyComment } from '../../../common/utils/markdown_plugins/utils';
import { ConnectorTypes, SECURITY_SOLUTION_OWNER } from '../../../common';
export { caseMigrations } from './cases';
export { configureMigrations } from './configuration';
export { createCommentsMigrations, CreateCommentsMigrationsDeps } from './comments';
interface UserActions {
action_field: string[];
@ -99,97 +84,6 @@ export const userActionsMigrations = {
},
};
interface UnsanitizedComment {
comment: string;
type?: CommentType;
}
interface SanitizedComment {
comment: string;
type: CommentType;
}
interface SanitizedCommentForSubCases {
associationType: AssociationType;
rule?: { id: string | null; name: string | 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 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<MigrateFunctionsObject, SavedObjectMigrationFn>(
migrationDeps.lensEmbeddableFactory().migrations,
migrateByValueLensVisualizations
) as MigrateFunctionsObject;
const commentsMigrations = {
'7.11.0': (
doc: SavedObjectUnsanitizedDoc<UnsanitizedComment>
): SavedObjectSanitizedDoc<SanitizedComment> => {
return {
...doc,
attributes: {
...doc.attributes,
type: CommentType.user,
},
references: doc.references || [],
};
},
'7.12.0': (
doc: SavedObjectUnsanitizedDoc<UnsanitizedComment>
): SavedObjectSanitizedDoc<SanitizedCommentForSubCases> => {
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': (
doc: SavedObjectUnsanitizedDoc<Record<string, unknown>>
): SavedObjectSanitizedDoc<SanitizedCaseOwner> => {
return addOwnerToSO(doc);
},
};
return mergeMigrationFunctionMaps(commentsMigrations, embeddableMigrations);
};
export const connectorMappingsMigrations = {
'7.14.0': (
doc: SavedObjectUnsanitizedDoc<Record<string, unknown>>