Skip to content

Commit

Permalink
Use markup by formatters, it's escaped for dangerouslySetInnerHTML
Browse files Browse the repository at this point in the history
  • Loading branch information
kertal committed Oct 25, 2019
1 parent a68c24f commit 84619bd
Show file tree
Hide file tree
Showing 5 changed files with 14 additions and 212 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
import React, { useState } from 'react';
import { DocViewRenderProps } from 'ui/registry/doc_views';
import { DocViewTableRow } from './table_row';
import { arrayContainsObjects } from './table_helper';
import { arrayContainsObjects, trimAngularSpan } from './table_helper';

const COLLAPSE_LINE_LENGTH = 350;

Expand Down Expand Up @@ -47,15 +47,10 @@ export function DocViewTable({
{Object.keys(flattened)
.sort()
.map(field => {
function trimAngularSpan(text: string) {
return text.replace('<span ng-non-bindable>', '').replace('</span>', '');
}
const valueRaw = flattened[field];
const valueFormattedTrimmed = trimAngularSpan(formatted[field]);
const isFormatted = valueRaw !== valueFormattedTrimmed;
const value = isFormatted ? valueFormattedTrimmed : valueRaw;
const value = trimAngularSpan(String(formatted[field]));

const isCollapsible = typeof value === 'string' && value.length > COLLAPSE_LINE_LENGTH;
const isCollapsible = value.length > COLLAPSE_LINE_LENGTH;
const isCollapsed = isCollapsible && !fieldRowOpen[field];
const toggleColumn =
onRemoveColumn && onAddColumn && Array.isArray(columns)
Expand Down Expand Up @@ -83,7 +78,6 @@ export function DocViewTable({
isCollapsed={isCollapsed}
isCollapsible={isCollapsible}
isColumnActive={Array.isArray(columns) && columns.includes(field)}
isFormatted={isFormatted}
onFilter={filter}
onToggleCollapse={() => toggleValueCollapse(field)}
onToggleColumn={toggleColumn}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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`, () => {
Expand Down Expand Up @@ -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>
`);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -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>', '');
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@ export interface Props {
isCollapsible: boolean;
isColumnActive: boolean;
isCollapsed: boolean;
isFormatted: boolean;
onToggleCollapse: () => void;
onFilter?: DocViewFilterFn;
onToggleColumn?: () => void;
Expand All @@ -52,7 +51,6 @@ export function DocViewTableRow({
isCollapsible,
isCollapsed,
isColumnActive,
isFormatted,
onFilter,
onToggleCollapse,
onToggleColumn,
Expand Down Expand Up @@ -95,17 +93,11 @@ export function DocViewTableRow({
)}
{displayUnderscoreWarning && <DocViewTableRowIconUnderscore />}
{displayNoMappingWarning && <DocViewTableRowIconNoMapping />}
{isFormatted ? (
<div
className={valueClassName}
data-test-subj={`tableDocViewRow-${field}-value`}
dangerouslySetInnerHTML={{ __html: value as string }}
/>
) : (
<div className={valueClassName} data-test-subj={`tableDocViewRow-${field}-value`}>
{value}
</div>
)}
<div
className={valueClassName}
data-test-subj={`tableDocViewRow-${field}-value`}
dangerouslySetInnerHTML={{ __html: value as string }}
/>
</td>
</tr>
);
Expand Down

0 comments on commit 84619bd

Please sign in to comment.