From 7d26fc16dc746200d98ea0832a3c7c44a1421020 Mon Sep 17 00:00:00 2001 From: Matthias Wilhelm Date: Mon, 21 Oct 2019 10:14:33 +0200 Subject: [PATCH] reactify field formatters initial --- .../index_patterns/format_hit.ts | 33 ++++- .../public/views/table/table.tsx | 8 +- .../public/views/table/table_helper.test.ts | 133 +----------------- .../public/views/table/table_helper.tsx | 60 -------- .../field_formats/types/{url.ts => url.tsx} | 42 ++++-- ..._content_type.ts => html_content_type.tsx} | 34 +++-- .../data/common/field_formats/field_format.ts | 9 +- .../data/common/field_formats/types.ts | 6 +- .../utils/highlight/highlight.ts | 29 ++++ .../utils/highlight/highlight_react.tsx | 49 +++++++ .../field_formats/utils/highlight/index.ts | 1 + .../data/common/field_formats/utils/index.ts | 2 +- 12 files changed, 181 insertions(+), 225 deletions(-) rename src/legacy/core_plugins/kibana/common/field_formats/types/{url.ts => url.tsx} (85%) rename src/plugins/data/common/field_formats/content_types/{html_content_type.ts => html_content_type.tsx} (69%) create mode 100644 src/plugins/data/common/field_formats/utils/highlight/highlight.ts create mode 100644 src/plugins/data/common/field_formats/utils/highlight/highlight_react.tsx diff --git a/src/legacy/core_plugins/data/public/index_patterns/index_patterns/format_hit.ts b/src/legacy/core_plugins/data/public/index_patterns/index_patterns/format_hit.ts index 02d61f8b32c86..b88e82d190b9f 100644 --- a/src/legacy/core_plugins/data/public/index_patterns/index_patterns/format_hit.ts +++ b/src/legacy/core_plugins/data/public/index_patterns/index_patterns/format_hit.ts @@ -19,6 +19,7 @@ import _ from 'lodash'; import { IndexPattern } from './index_pattern'; +import { asPrettyString } from '../../../../../../plugins/data/common/field_formats/utils'; const formattedCache = new WeakMap(); const partialFormattedCache = new WeakMap(); @@ -26,17 +27,32 @@ const partialFormattedCache = new WeakMap(); // Takes a hit, merges it with any stored/scripted fields, and with the metaFields // returns a formatted version export function formatHitProvider(indexPattern: IndexPattern, defaultFormat: any) { - function convert(hit: Record, val: any, fieldName: string, type: string = 'html') { + function convert( + hit: Record, + val: any, + fieldName: string, + type: string = 'html', + returnReact = false + ) { const field = indexPattern.fields.getByName(fieldName); - if (!field) return defaultFormat.convert(val, type); + if (!field) { + if (returnReact) { + return asPrettyString(val); + } + return defaultFormat.convert(val, type); + } const parsedUrl = { origin: window.location.origin, pathname: window.location.pathname, }; - return field.format.getConverterFor(type)(val, field, hit, parsedUrl); + return field.format.getConverterFor(type)(val, field, hit, parsedUrl, returnReact); } - function formatHit(hit: Record, type: string = 'html') { + function formatHit( + hit: Record, + type: string = 'html', + returnReact: boolean = false + ) { if (type === 'text') { // formatHit of type text is for react components to get rid of // since it's currently just used at the discover's doc view table, caching is not necessary @@ -48,6 +64,15 @@ export function formatHitProvider(indexPattern: IndexPattern, defaultFormat: any return result; } + if (type === 'html' && returnReact) { + const flattened = indexPattern.flattenHit(hit); + const result: Record = {}; + for (const [key, value] of Object.entries(flattened)) { + result[key] = convert(hit, value, key, type, returnReact); + } + return result; + } + const cached = formattedCache.get(hit); if (cached) { return cached; diff --git a/src/legacy/core_plugins/kbn_doc_views/public/views/table/table.tsx b/src/legacy/core_plugins/kbn_doc_views/public/views/table/table.tsx index 8309eaa403f4c..a677e8c9cebce 100644 --- a/src/legacy/core_plugins/kbn_doc_views/public/views/table/table.tsx +++ b/src/legacy/core_plugins/kbn_doc_views/public/views/table/table.tsx @@ -19,7 +19,7 @@ import React, { useState } from 'react'; import { DocViewRenderProps } from 'ui/registry/doc_views'; import { DocViewTableRow } from './table_row'; -import { formatValue, arrayContainsObjects } from './table_helper'; +import { arrayContainsObjects } from './table_helper'; const COLLAPSE_LINE_LENGTH = 350; @@ -33,11 +33,11 @@ export function DocViewTable({ }: DocViewRenderProps) { const mapping = indexPattern.fields.getByName; const flattened = indexPattern.flattenHit(hit); - const formatted = indexPattern.formatHit(hit, 'html'); + const formatted = indexPattern.formatHit(hit, 'html', true); const [fieldRowOpen, setFieldRowOpen] = useState({} as Record); function toggleValueCollapse(field: string) { - fieldRowOpen[field] = fieldRowOpen[field] !== true; + fieldRowOpen[field] = !fieldRowOpen[field]; setFieldRowOpen({ ...fieldRowOpen }); } @@ -48,7 +48,7 @@ export function DocViewTable({ .sort() .map(field => { const valueRaw = flattened[field]; - const value = formatValue(valueRaw, formatted[field]); + const value = formatted[field]; const isCollapsible = typeof value === 'string' && value.length > COLLAPSE_LINE_LENGTH; const isCollapsed = isCollapsible && !fieldRowOpen[field]; const toggleColumn = diff --git a/src/legacy/core_plugins/kbn_doc_views/public/views/table/table_helper.test.ts b/src/legacy/core_plugins/kbn_doc_views/public/views/table/table_helper.test.ts index f075e06c7651f..2402d4dddb874 100644 --- a/src/legacy/core_plugins/kbn_doc_views/public/views/table/table_helper.test.ts +++ b/src/legacy/core_plugins/kbn_doc_views/public/views/table/table_helper.test.ts @@ -16,91 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -import { - replaceMarkWithReactDom, - convertAngularHtml, - arrayContainsObjects, - formatValue, -} from './table_helper'; - -describe('replaceMarkWithReactDom', () => { - it(`converts test to react nodes`, () => { - const actual = replaceMarkWithReactDom( - 'marked1 blablabla marked2 end' - ); - expect(actual).toMatchInlineSnapshot(` - - - - - marked1 - - blablabla - - - - marked2 - - end - - - `); - }); - - it(`doesn't convert invalid markup to react dom nodes`, () => { - const actual = replaceMarkWithReactDom('test sdf sdf'); - expect(actual).toMatchInlineSnapshot(` - - - test sdf - - - sdf - - - - - `); - }); - - it(`returns strings without markup unchanged `, () => { - const actual = replaceMarkWithReactDom('blablabla'); - expect(actual).toMatchInlineSnapshot(` - - blablabla - - `); - }); -}); - -describe('convertAngularHtml', () => { - it(`converts html for usage in angular to usage in react`, () => { - const actual = convertAngularHtml('Good morning!'); - expect(actual).toMatchInlineSnapshot(`"Good morning!"`); - }); - it(`converts html containing for usage in react`, () => { - const actual = convertAngularHtml( - 'Good morningdear reviewer!' - ); - expect(actual).toMatchInlineSnapshot(` - - Good - - - morning - - dear - - - - reviewer - - ! - - - `); - }); -}); +import { arrayContainsObjects } from './table_helper'; describe('arrayContainsObjects', () => { it(`returns false for an array of primitives`, () => { @@ -128,50 +44,3 @@ describe('arrayContainsObjects', () => { expect(actual).toBeFalsy(); }); }); - -describe('formatValue', () => { - it(`formats an array of objects`, () => { - const actual = formatValue([{ test: '123' }, ''], ''); - expect(actual).toMatchInlineSnapshot(` - "{ - \\"test\\": \\"123\\" - } - \\"\\"" - `); - }); - it(`formats an array of primitives`, () => { - const actual = formatValue(['test1', 'test2'], ''); - expect(actual).toMatchInlineSnapshot(`"test1, test2"`); - }); - it(`formats an object`, () => { - const actual = formatValue({ test: 1 }, ''); - expect(actual).toMatchInlineSnapshot(` - "{ - \\"test\\": 1 - }" - `); - }); - it(`formats an angular formatted string `, () => { - const actual = formatValue( - '', - 'Good morningdear reviewer!' - ); - expect(actual).toMatchInlineSnapshot(` - - Good - - - morning - - dear - - - - reviewer - - ! - - - `); - }); -}); diff --git a/src/legacy/core_plugins/kbn_doc_views/public/views/table/table_helper.tsx b/src/legacy/core_plugins/kbn_doc_views/public/views/table/table_helper.tsx index e959ec336bf3a..f22393be8de88 100644 --- a/src/legacy/core_plugins/kbn_doc_views/public/views/table/table_helper.tsx +++ b/src/legacy/core_plugins/kbn_doc_views/public/views/table/table_helper.tsx @@ -16,70 +16,10 @@ * specific language governing permissions and limitations * under the License. */ -import React from 'react'; -import { unescape } from 'lodash'; -/** - * Convert markup of the given string to ReactNodes - * @param text - */ -export function replaceMarkWithReactDom(text: string): React.ReactNode { - return ( - <> - {text.split('').map((markedText, idx) => { - const sub = markedText.split(''); - if (sub.length === 1) { - return markedText; - } - return ( - - {sub[0]} - {sub[1]} - - ); - })} - - ); -} - -/** - * Current html of the formatter is angular flavored, this current workaround - * should be removed when all consumers of the formatHit function are react based - */ -export function convertAngularHtml(html: string): string | React.ReactNode { - if (typeof html === 'string') { - const cleaned = html.replace('', '').replace('', ''); - const unescaped = unescape(cleaned); - if (unescaped.indexOf('') !== -1) { - return replaceMarkWithReactDom(unescaped); - } - return unescaped; - } - return html; -} /** * Returns true if the given array contains at least 1 object */ export function arrayContainsObjects(value: unknown[]) { return Array.isArray(value) && value.some(v => typeof v === 'object' && v !== null); } - -/** - * The current field formatter provides html for angular usage - * This html is cleaned up and prepared for usage in the react world - * Furthermore test are converted to ReactNodes - */ -export function formatValue( - value: null | string | number | boolean | object | Array, - valueFormatted: string -): string | React.ReactNode { - if (Array.isArray(value) && arrayContainsObjects(value)) { - return value.map(v => JSON.stringify(v, null, 2)).join('\n'); - } else if (Array.isArray(value)) { - return value.join(', '); - } else if (typeof value === 'object' && value !== null) { - return JSON.stringify(value, null, 2); - } else { - return typeof valueFormatted === 'string' ? convertAngularHtml(valueFormatted) : String(value); - } -} diff --git a/src/legacy/core_plugins/kibana/common/field_formats/types/url.ts b/src/legacy/core_plugins/kibana/common/field_formats/types/url.tsx similarity index 85% rename from src/legacy/core_plugins/kibana/common/field_formats/types/url.ts rename to src/legacy/core_plugins/kibana/common/field_formats/types/url.tsx index e9eb41513d52d..e7b42ad6b1923 100644 --- a/src/legacy/core_plugins/kibana/common/field_formats/types/url.ts +++ b/src/legacy/core_plugins/kibana/common/field_formats/types/url.tsx @@ -16,16 +16,16 @@ * specific language governing permissions and limitations * under the License. */ - +import React, { ReactElement } from 'react'; import { i18n } from '@kbn/i18n'; import { escape, memoize } from 'lodash'; import { - getHighlightHtml, FieldFormat, KBN_FIELD_TYPES, TextContextTypeConvert, HtmlContextTypeConvert, } from '../../../../../../plugins/data/common/'; +import { getHighlight } from '../../../../../../plugins/data/common/field_formats/utils/highlight'; const templateMatchRE = /{{([\s\S]+?)}}/g; const whitelistUrlSchemes = ['http://', 'https://']; @@ -127,23 +127,43 @@ export function createUrlFormat() { }; } - private generateImgHtml(url: string, imageLabel: string): string { + private generateImgHtml( + url: string, + imageLabel: string, + returnReact: boolean = false + ): string | ReactElement { const isValidWidth = !isNaN(parseInt(this.param('width'), 10)); const isValidHeight = !isNaN(parseInt(this.param('height'), 10)); const maxWidth = isValidWidth ? `${this.param('width')}px` : 'none'; const maxHeight = isValidHeight ? `${this.param('height')}px` : 'none'; - + if (returnReact) { + return ( + {imageLabel} + ); + } return `${imageLabel}`; } textConvert: TextContextTypeConvert = value => this.formatLabel(value); - htmlConvert: HtmlContextTypeConvert = (rawValue, field, hit, parsedUrl) => { + htmlConvert: HtmlContextTypeConvert = (rawValue, field, hit, parsedUrl, returnReact) => { const url = escape(this.formatUrl(rawValue)); const label = escape(this.formatLabel(rawValue, url)); switch (this.param('type')) { case 'audio': + if (returnReact) { + return ( + + ); + } + return `