Skip to content

Commit

Permalink
reactify field formatters initial
Browse files Browse the repository at this point in the history
  • Loading branch information
kertal committed Oct 21, 2019
1 parent 432c494 commit 7d26fc1
Show file tree
Hide file tree
Showing 12 changed files with 181 additions and 225 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -19,24 +19,40 @@

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();

// 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<string, any>, val: any, fieldName: string, type: string = 'html') {
function convert(
hit: Record<string, any>,
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<string, any>, type: string = 'html') {
function formatHit(
hit: Record<string, any>,
type: string = 'html',
returnReact: boolean = false
) {
if (type === 'text') {
// formatHit of type text is for react components to get rid of <span ng-non-bindable>
// since it's currently just used at the discover's doc view table, caching is not necessary
Expand All @@ -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<string, any> = {};
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;
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 { formatValue, arrayContainsObjects } from './table_helper';
import { arrayContainsObjects } from './table_helper';

const COLLAPSE_LINE_LENGTH = 350;

Expand All @@ -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<string, boolean>);

function toggleValueCollapse(field: string) {
fieldRowOpen[field] = fieldRowOpen[field] !== true;
fieldRowOpen[field] = !fieldRowOpen[field];
setFieldRowOpen({ ...fieldRowOpen });
}

Expand All @@ -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 =
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,10 @@
* 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[]) {
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
*/
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);
}
}
Loading

0 comments on commit 7d26fc1

Please sign in to comment.