mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
* Add detection if a value has been formatted, conditional rendering * Use markup by formatters, it's escaped for dangerouslySetInnerHTML * Enable dangerouslySetInnerHTML for displaying values * Use regex for replace
This commit is contained in:
parent
8a52aad405
commit
7f72b71b34
5 changed files with 20 additions and 197 deletions
|
@ -65,7 +65,7 @@ const indexPattern = {
|
|||
},
|
||||
metaFields: ['_index', '_score'],
|
||||
flattenHit: undefined,
|
||||
formatHit: jest.fn(hit => hit),
|
||||
formatHit: jest.fn(hit => hit._source),
|
||||
} as IndexPattern;
|
||||
|
||||
indexPattern.flattenHit = flattenHitWrapper(indexPattern, indexPattern.metaFields);
|
||||
|
|
|
@ -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, trimAngularSpan } from './table_helper';
|
||||
|
||||
const COLLAPSE_LINE_LENGTH = 350;
|
||||
|
||||
|
@ -48,8 +48,9 @@ export function DocViewTable({
|
|||
.sort()
|
||||
.map(field => {
|
||||
const valueRaw = flattened[field];
|
||||
const value = formatValue(valueRaw, formatted[field]);
|
||||
const isCollapsible = typeof value === 'string' && value.length > COLLAPSE_LINE_LENGTH;
|
||||
const value = trimAngularSpan(String(formatted[field]));
|
||||
|
||||
const isCollapsible = value.length > COLLAPSE_LINE_LENGTH;
|
||||
const isCollapsed = isCollapsible && !fieldRowOpen[field];
|
||||
const toggleColumn =
|
||||
onRemoveColumn && onAddColumn && Array.isArray(columns)
|
||||
|
|
|
@ -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 <mark>test</mark> to react nodes`, () => {
|
||||
const actual = replaceMarkWithReactDom(
|
||||
'<mark>marked1</mark> blablabla <mark>marked2</mark> end'
|
||||
);
|
||||
expect(actual).toMatchInlineSnapshot(`
|
||||
<React.Fragment>
|
||||
|
||||
<span>
|
||||
<mark>
|
||||
marked1
|
||||
</mark>
|
||||
blablabla
|
||||
</span>
|
||||
<span>
|
||||
<mark>
|
||||
marked2
|
||||
</mark>
|
||||
end
|
||||
</span>
|
||||
</React.Fragment>
|
||||
`);
|
||||
});
|
||||
|
||||
it(`doesn't convert invalid markup to react dom nodes`, () => {
|
||||
const actual = replaceMarkWithReactDom('<mark>test sdf <mark>sdf</mark>');
|
||||
expect(actual).toMatchInlineSnapshot(`
|
||||
<React.Fragment>
|
||||
|
||||
test sdf
|
||||
<span>
|
||||
<mark>
|
||||
sdf
|
||||
</mark>
|
||||
|
||||
</span>
|
||||
</React.Fragment>
|
||||
`);
|
||||
});
|
||||
|
||||
it(`returns strings without markup unchanged `, () => {
|
||||
const actual = replaceMarkWithReactDom('blablabla');
|
||||
expect(actual).toMatchInlineSnapshot(`
|
||||
<React.Fragment>
|
||||
blablabla
|
||||
</React.Fragment>
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('convertAngularHtml', () => {
|
||||
it(`converts html for usage in angular to usage in react`, () => {
|
||||
const actual = convertAngularHtml('<span ng-non-bindable>Good morning!</span>');
|
||||
expect(actual).toMatchInlineSnapshot(`"Good morning!"`);
|
||||
});
|
||||
it(`converts html containing <mark> for usage in react`, () => {
|
||||
const actual = convertAngularHtml(
|
||||
'<span ng-non-bindable>Good <mark>morning</mark>dear <mark>reviewer</mark>!</span>'
|
||||
);
|
||||
expect(actual).toMatchInlineSnapshot(`
|
||||
<React.Fragment>
|
||||
Good
|
||||
<span>
|
||||
<mark>
|
||||
morning
|
||||
</mark>
|
||||
dear
|
||||
</span>
|
||||
<span>
|
||||
<mark>
|
||||
reviewer
|
||||
</mark>
|
||||
!
|
||||
</span>
|
||||
</React.Fragment>
|
||||
`);
|
||||
});
|
||||
});
|
||||
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(
|
||||
'',
|
||||
'<span ng-non-bindable>Good <mark>morning</mark>dear <mark>reviewer</mark>!</span>'
|
||||
);
|
||||
expect(actual).toMatchInlineSnapshot(`
|
||||
<React.Fragment>
|
||||
Good
|
||||
<span>
|
||||
<mark>
|
||||
morning
|
||||
</mark>
|
||||
dear
|
||||
</span>
|
||||
<span>
|
||||
<mark>
|
||||
reviewer
|
||||
</mark>
|
||||
!
|
||||
</span>
|
||||
</React.Fragment>
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -16,70 +16,17 @@
|
|||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import React from 'react';
|
||||
import { unescape } from 'lodash';
|
||||
|
||||
/**
|
||||
* Convert <mark> markup of the given string to ReactNodes
|
||||
* @param text
|
||||
*/
|
||||
export function replaceMarkWithReactDom(text: string): React.ReactNode {
|
||||
return (
|
||||
<>
|
||||
{text.split('<mark>').map((markedText, idx) => {
|
||||
const sub = markedText.split('</mark>');
|
||||
if (sub.length === 1) {
|
||||
return markedText;
|
||||
}
|
||||
return (
|
||||
<span key={idx}>
|
||||
<mark>{sub[0]}</mark>
|
||||
{sub[1]}
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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('<span ng-non-bindable>', '').replace('</span>', '');
|
||||
const unescaped = unescape(cleaned);
|
||||
if (unescaped.indexOf('<mark>') !== -1) {
|
||||
return replaceMarkWithReactDom(unescaped);
|
||||
}
|
||||
return unescaped;
|
||||
}
|
||||
return html;
|
||||
}
|
||||
/**
|
||||
* Returns true if the given array contains at least 1 object
|
||||
*/
|
||||
export function arrayContainsObjects(value: unknown[]) {
|
||||
export function arrayContainsObjects(value: unknown[]): boolean {
|
||||
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 <mark>test</mark> are converted to ReactNodes
|
||||
* Removes markup added by kibana fields html formatter
|
||||
*/
|
||||
export function formatValue(
|
||||
value: null | string | number | boolean | object | Array<string | object | null>,
|
||||
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);
|
||||
}
|
||||
export function trimAngularSpan(text: string): string {
|
||||
return text.replace(/^<span ng-non-bindable>/, '').replace(/<\/span>$/, '');
|
||||
}
|
||||
|
|
|
@ -85,7 +85,7 @@ export function DocViewTableRow({
|
|||
</td>
|
||||
)}
|
||||
<td className="kbnDocViewer__field">
|
||||
<FieldName field={fieldMapping} fieldName={field}></FieldName>
|
||||
<FieldName field={fieldMapping} fieldName={field} />
|
||||
</td>
|
||||
<td>
|
||||
{isCollapsible && (
|
||||
|
@ -93,9 +93,15 @@ export function DocViewTableRow({
|
|||
)}
|
||||
{displayUnderscoreWarning && <DocViewTableRowIconUnderscore />}
|
||||
{displayNoMappingWarning && <DocViewTableRowIconNoMapping />}
|
||||
<div className={valueClassName} data-test-subj={`tableDocViewRow-${field}-value`}>
|
||||
{value}
|
||||
</div>
|
||||
<div
|
||||
className={valueClassName}
|
||||
data-test-subj={`tableDocViewRow-${field}-value`}
|
||||
/*
|
||||
* Justification for dangerouslySetInnerHTML:
|
||||
* We just use values encoded by our field formatters
|
||||
*/
|
||||
dangerouslySetInnerHTML={{ __html: value as string }}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue