Support linkify-ing urls
Summary: Changelog: The new logs plugin will linkify urls and pretty print json-like messages This implements one of our top papercuts (see linked task), and the WP request over here: https://fb.workplace.com/groups/flipperfyi/permalink/902949260471370/. Partially addresses https://github.com/facebook/flipper/issues/1162 https://github.com/facebook/flipper/issues/1010 https://github.com/facebook/flipper/issues/2029 Reviewed By: nikoant Differential Revision: D26947007 fbshipit-source-id: be0fdb476765905ae6b63bd8799c9c6093014de3
This commit is contained in:
committed by
Facebook GitHub Bot
parent
66774c90c6
commit
d73f6578a7
@@ -28,6 +28,7 @@ test('Correct top level API exposed', () => {
|
|||||||
// Note, all `exposedAPIs` should be documented in `flipper-plugin.mdx`
|
// Note, all `exposedAPIs` should be documented in `flipper-plugin.mdx`
|
||||||
expect(exposedAPIs.sort()).toMatchInlineSnapshot(`
|
expect(exposedAPIs.sort()).toMatchInlineSnapshot(`
|
||||||
Array [
|
Array [
|
||||||
|
"DataFormatter",
|
||||||
"DataSource",
|
"DataSource",
|
||||||
"DataTable",
|
"DataTable",
|
||||||
"Layout",
|
"Layout",
|
||||||
|
|||||||
@@ -65,6 +65,8 @@ export {
|
|||||||
wrapInteractionHandler as _wrapInteractionHandler,
|
wrapInteractionHandler as _wrapInteractionHandler,
|
||||||
} from './ui/Tracked';
|
} from './ui/Tracked';
|
||||||
|
|
||||||
|
export {DataFormatter} from './ui/DataFormatter';
|
||||||
|
|
||||||
export {sleep} from './utils/sleep';
|
export {sleep} from './utils/sleep';
|
||||||
export {
|
export {
|
||||||
LogTypes,
|
LogTypes,
|
||||||
|
|||||||
@@ -36,7 +36,9 @@ type DataSourceish = DataSource<Todo> & FakeDataSource<Todo>;
|
|||||||
|
|
||||||
// NOTE: this run in jest, which is not optimal for perf, but should give some idea
|
// NOTE: this run in jest, which is not optimal for perf, but should give some idea
|
||||||
// make sure to use the `yarn watch` script in desktop root, so that the garbage collector is exposed
|
// make sure to use the `yarn watch` script in desktop root, so that the garbage collector is exposed
|
||||||
test('run perf test', () => {
|
|
||||||
|
// By default skipped to not slow down each and every test run
|
||||||
|
test.skip('run perf test', () => {
|
||||||
if (!global.gc) {
|
if (!global.gc) {
|
||||||
console.warn(
|
console.warn(
|
||||||
'Warning: garbage collector not available, skipping this test',
|
'Warning: garbage collector not available, skipping this test',
|
||||||
|
|||||||
121
desktop/flipper-plugin/src/ui/DataFormatter.tsx
Normal file
121
desktop/flipper-plugin/src/ui/DataFormatter.tsx
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the MIT license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree.
|
||||||
|
*
|
||||||
|
* @format
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {Typography} from 'antd';
|
||||||
|
import {pad} from 'lodash';
|
||||||
|
import React, {createElement, Fragment, isValidElement} from 'react';
|
||||||
|
import {safeStringify} from '../utils/safeStringify';
|
||||||
|
import {urlRegex} from '../utils/urlRegex';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A Formatter is used to render an arbitrarily value to React. If a formatter returns 'undefined'
|
||||||
|
* it is considered a 'miss' and the next formatter will be tried, eventually falling back to the default formatter.
|
||||||
|
*
|
||||||
|
* In case further processing by the default formatter is to be avoided, make sure a string is returned from any custom formatter.
|
||||||
|
*/
|
||||||
|
export type Formatter = (value: any) => string | React.ReactElement | any;
|
||||||
|
|
||||||
|
export const DataFormatter = {
|
||||||
|
defaultFormatter(value: any) {
|
||||||
|
if (isValidElement(value)) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
switch (typeof value) {
|
||||||
|
case 'boolean':
|
||||||
|
return value ? 'true' : 'false';
|
||||||
|
case 'number':
|
||||||
|
return '' + value;
|
||||||
|
case 'undefined':
|
||||||
|
return '';
|
||||||
|
case 'string':
|
||||||
|
return value;
|
||||||
|
case 'object': {
|
||||||
|
if (value === null) return '';
|
||||||
|
if (value instanceof Date) {
|
||||||
|
return (
|
||||||
|
value.toTimeString().split(' ')[0] +
|
||||||
|
'.' +
|
||||||
|
pad('' + value.getMilliseconds(), 3)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (value instanceof Map) {
|
||||||
|
return safeStringify(Array.from(value.entries()));
|
||||||
|
}
|
||||||
|
if (value instanceof Set) {
|
||||||
|
return safeStringify(Array.from(value.values()));
|
||||||
|
}
|
||||||
|
return safeStringify(value);
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return '<unrenderable value>';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Formatter that will automatically create links for any urls inside the data
|
||||||
|
*/
|
||||||
|
linkify(value: any) {
|
||||||
|
if (typeof value === 'string' && urlRegex.test(value)) {
|
||||||
|
return createElement(
|
||||||
|
Fragment,
|
||||||
|
undefined,
|
||||||
|
// spreading children avoids the need for keys and reconciles by index
|
||||||
|
...value.split(urlRegex).map((part, index) =>
|
||||||
|
// odd items are the links
|
||||||
|
index % 2 === 1 ? (
|
||||||
|
<Typography.Link href={part}>{part}</Typography.Link>
|
||||||
|
) : (
|
||||||
|
part
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
},
|
||||||
|
|
||||||
|
prettyPrintJson(value: any) {
|
||||||
|
if (typeof value === 'string' && value.length >= 2) {
|
||||||
|
const last = value.length - 1;
|
||||||
|
// kinda looks like json
|
||||||
|
|
||||||
|
if (
|
||||||
|
(value[0] === '{' && value[last] === '}') ||
|
||||||
|
(value[0] === '[' && value[last] === ']')
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
value = JSON.parse(value);
|
||||||
|
} catch (e) {
|
||||||
|
// intentional fall through, can't parse this 'json'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (typeof value === 'object' && value !== null) {
|
||||||
|
try {
|
||||||
|
// Note: we don't need to be inserted <br/>'s in the output, but assume the text container uses
|
||||||
|
// white-space: pre-wrap (or pre)
|
||||||
|
return JSON.stringify(value, null, 2);
|
||||||
|
} catch (e) {
|
||||||
|
// intentional fall through, can't pretty print this 'json'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
},
|
||||||
|
|
||||||
|
format(value: any, formatters?: Formatter[] | Formatter): any {
|
||||||
|
let res = value;
|
||||||
|
if (Array.isArray(formatters)) {
|
||||||
|
for (const formatter of formatters) {
|
||||||
|
res = formatter(res);
|
||||||
|
}
|
||||||
|
} else if (formatters) {
|
||||||
|
res = formatters(res);
|
||||||
|
}
|
||||||
|
return DataFormatter.defaultFormatter(res);
|
||||||
|
},
|
||||||
|
};
|
||||||
164
desktop/flipper-plugin/src/ui/__tests__/DataFormatter.node.tsx
Normal file
164
desktop/flipper-plugin/src/ui/__tests__/DataFormatter.node.tsx
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the MIT license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree.
|
||||||
|
*
|
||||||
|
* @format
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {DataFormatter} from '../DataFormatter';
|
||||||
|
|
||||||
|
test('default formatter', () => {
|
||||||
|
expect(DataFormatter.format(true)).toMatchInlineSnapshot(`"true"`);
|
||||||
|
expect(DataFormatter.format(false)).toMatchInlineSnapshot(`"false"`);
|
||||||
|
expect(DataFormatter.format(3)).toMatchInlineSnapshot(`"3"`);
|
||||||
|
expect(DataFormatter.format(null)).toMatchInlineSnapshot(`""`);
|
||||||
|
expect(DataFormatter.format(undefined)).toMatchInlineSnapshot(`""`);
|
||||||
|
expect(
|
||||||
|
DataFormatter.format(new Date(2020, 2, 3, 5, 8, 4, 244654)),
|
||||||
|
).toMatchInlineSnapshot(`"05:12:08.654"`);
|
||||||
|
expect(DataFormatter.format('test')).toMatchInlineSnapshot(`"test"`);
|
||||||
|
|
||||||
|
expect(DataFormatter.format({hello: 'world'})).toMatchInlineSnapshot(`
|
||||||
|
"{
|
||||||
|
\\"hello\\": \\"world\\"
|
||||||
|
}"
|
||||||
|
`);
|
||||||
|
expect(DataFormatter.format({hello: ['world']})).toMatchInlineSnapshot(`
|
||||||
|
"{
|
||||||
|
\\"hello\\": [
|
||||||
|
\\"world\\"
|
||||||
|
]
|
||||||
|
}"
|
||||||
|
`);
|
||||||
|
expect(DataFormatter.format(new Map([['hello', 'world']])))
|
||||||
|
.toMatchInlineSnapshot(`
|
||||||
|
"[
|
||||||
|
[
|
||||||
|
\\"hello\\",
|
||||||
|
\\"world\\"
|
||||||
|
]
|
||||||
|
]"
|
||||||
|
`);
|
||||||
|
expect(DataFormatter.format(new Set([['hello', 'world']])))
|
||||||
|
.toMatchInlineSnapshot(`
|
||||||
|
"[
|
||||||
|
[
|
||||||
|
\\"hello\\",
|
||||||
|
\\"world\\"
|
||||||
|
]
|
||||||
|
]"
|
||||||
|
`);
|
||||||
|
|
||||||
|
const unserializable: any = {};
|
||||||
|
unserializable.x = unserializable;
|
||||||
|
expect(DataFormatter.format(unserializable)).toMatchInlineSnapshot(`
|
||||||
|
"<Failed to serialize: TypeError: Converting circular structure to JSON
|
||||||
|
--> starting at object with constructor 'Object'
|
||||||
|
--- property 'x' closes the circle>"
|
||||||
|
`);
|
||||||
|
|
||||||
|
// make sure we preserve newlines
|
||||||
|
expect(DataFormatter.format('Test 123\n\t\t345\n\t\t67'))
|
||||||
|
.toMatchInlineSnapshot(`
|
||||||
|
"Test 123
|
||||||
|
345
|
||||||
|
67"
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('linkify formatter', () => {
|
||||||
|
const linkify = (value: any) =>
|
||||||
|
DataFormatter.format(value, DataFormatter.linkify);
|
||||||
|
|
||||||
|
// verify fallback
|
||||||
|
expect(linkify({hello: 'world'})).toMatchInlineSnapshot(`
|
||||||
|
"{
|
||||||
|
\\"hello\\": \\"world\\"
|
||||||
|
}"
|
||||||
|
`);
|
||||||
|
expect(linkify('hi there!')).toMatchInlineSnapshot(`"hi there!"`);
|
||||||
|
expect(linkify('https://www.google.com')).toMatchInlineSnapshot(`
|
||||||
|
<React.Fragment>
|
||||||
|
|
||||||
|
<ForwardRef(Link)
|
||||||
|
href="https://www.google.com"
|
||||||
|
>
|
||||||
|
https://www.google.com
|
||||||
|
</ForwardRef(Link)>
|
||||||
|
|
||||||
|
</React.Fragment>
|
||||||
|
`);
|
||||||
|
expect(linkify('www.google.com')).toMatchInlineSnapshot(`"www.google.com"`);
|
||||||
|
expect(linkify('stuff.google.com')).toMatchInlineSnapshot(
|
||||||
|
`"stuff.google.com"`,
|
||||||
|
);
|
||||||
|
expect(linkify('test https://www.google.com test')).toMatchInlineSnapshot(`
|
||||||
|
<React.Fragment>
|
||||||
|
test
|
||||||
|
<ForwardRef(Link)
|
||||||
|
href="https://www.google.com"
|
||||||
|
>
|
||||||
|
https://www.google.com
|
||||||
|
</ForwardRef(Link)>
|
||||||
|
test
|
||||||
|
</React.Fragment>
|
||||||
|
`);
|
||||||
|
expect(linkify('https://www.google.com test http://fb.com'))
|
||||||
|
.toMatchInlineSnapshot(`
|
||||||
|
<React.Fragment>
|
||||||
|
|
||||||
|
<ForwardRef(Link)
|
||||||
|
href="https://www.google.com"
|
||||||
|
>
|
||||||
|
https://www.google.com
|
||||||
|
</ForwardRef(Link)>
|
||||||
|
test
|
||||||
|
<ForwardRef(Link)
|
||||||
|
href="http://fb.com"
|
||||||
|
>
|
||||||
|
http://fb.com
|
||||||
|
</ForwardRef(Link)>
|
||||||
|
|
||||||
|
</React.Fragment>
|
||||||
|
`);
|
||||||
|
expect(linkify('fb.com')).toMatchInlineSnapshot(`"fb.com"`);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('linkify formatter', () => {
|
||||||
|
const jsonify = (value: any) =>
|
||||||
|
DataFormatter.format(value, DataFormatter.prettyPrintJson);
|
||||||
|
|
||||||
|
expect(jsonify({hello: 'world'})).toMatchInlineSnapshot(`
|
||||||
|
"{
|
||||||
|
\\"hello\\": \\"world\\"
|
||||||
|
}"
|
||||||
|
`);
|
||||||
|
expect(jsonify([{hello: 'world'}])).toMatchInlineSnapshot(`
|
||||||
|
"[
|
||||||
|
{
|
||||||
|
\\"hello\\": \\"world\\"
|
||||||
|
}
|
||||||
|
]"
|
||||||
|
`);
|
||||||
|
// linkify json!
|
||||||
|
expect(
|
||||||
|
DataFormatter.format({hello: 'http://facebook.com'}, [
|
||||||
|
DataFormatter.prettyPrintJson,
|
||||||
|
DataFormatter.linkify,
|
||||||
|
]),
|
||||||
|
).toMatchInlineSnapshot(`
|
||||||
|
<React.Fragment>
|
||||||
|
{
|
||||||
|
"hello": "
|
||||||
|
<ForwardRef(Link)
|
||||||
|
href="http://facebook.com"
|
||||||
|
>
|
||||||
|
http://facebook.com
|
||||||
|
</ForwardRef(Link)>
|
||||||
|
"
|
||||||
|
}
|
||||||
|
</React.Fragment>
|
||||||
|
`);
|
||||||
|
});
|
||||||
@@ -45,6 +45,7 @@ import {Typography} from 'antd';
|
|||||||
import {CoffeeOutlined, SearchOutlined} from '@ant-design/icons';
|
import {CoffeeOutlined, SearchOutlined} from '@ant-design/icons';
|
||||||
import {useAssertStableRef} from '../../utils/useAssertStableRef';
|
import {useAssertStableRef} from '../../utils/useAssertStableRef';
|
||||||
import {TrackingScopeContext} from 'flipper-plugin/src/ui/Tracked';
|
import {TrackingScopeContext} from 'flipper-plugin/src/ui/Tracked';
|
||||||
|
import {Formatter} from '../DataFormatter';
|
||||||
|
|
||||||
interface DataTableProps<T = any> {
|
interface DataTableProps<T = any> {
|
||||||
columns: DataTableColumn<T>[];
|
columns: DataTableColumn<T>[];
|
||||||
@@ -62,6 +63,7 @@ export type DataTableColumn<T = any> = {
|
|||||||
key: keyof T & string;
|
key: keyof T & string;
|
||||||
// possible future extension: getValue(row) (and free-form key) to support computed columns
|
// possible future extension: getValue(row) (and free-form key) to support computed columns
|
||||||
onRender?: (row: T) => React.ReactNode;
|
onRender?: (row: T) => React.ReactNode;
|
||||||
|
formatters?: Formatter[] | Formatter;
|
||||||
title?: string;
|
title?: string;
|
||||||
width?: number | Percentage | undefined; // undefined: use all remaining width
|
width?: number | Percentage | undefined; // undefined: use all remaining width
|
||||||
wrap?: boolean;
|
wrap?: boolean;
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ import {
|
|||||||
Selection,
|
Selection,
|
||||||
} from './DataTableManager';
|
} from './DataTableManager';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import {normalizeCellValue} from './TableRow';
|
|
||||||
import {tryGetFlipperLibImplementation} from '../../plugin/FlipperLib';
|
import {tryGetFlipperLibImplementation} from '../../plugin/FlipperLib';
|
||||||
import {DataTableColumn} from './DataTable';
|
import {DataTableColumn} from './DataTable';
|
||||||
import {DataSource} from '../../state/DataSource';
|
import {DataSource} from '../../state/DataSource';
|
||||||
@@ -69,9 +68,7 @@ export function tableContextMenuFactory<T>(
|
|||||||
const items = getSelectedItems(datasource, selection);
|
const items = getSelectedItems(datasource, selection);
|
||||||
if (items.length) {
|
if (items.length) {
|
||||||
lib.writeTextToClipboard(
|
lib.writeTextToClipboard(
|
||||||
items
|
items.map((item) => '' + item[column.key]).join('\n'),
|
||||||
.map((item) => normalizeCellValue(item[column.key]))
|
|
||||||
.join('\n'),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}}>
|
}}>
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import styled from '@emotion/styled';
|
|||||||
import {theme} from 'flipper-plugin';
|
import {theme} from 'flipper-plugin';
|
||||||
import type {RenderContext} from './DataTable';
|
import type {RenderContext} from './DataTable';
|
||||||
import {Width} from '../../utils/widthUtils';
|
import {Width} from '../../utils/widthUtils';
|
||||||
import {pad} from 'lodash';
|
import {DataFormatter} from '../DataFormatter';
|
||||||
|
|
||||||
// heuristic for row estimation, should match any future styling updates
|
// heuristic for row estimation, should match any future styling updates
|
||||||
export const DEFAULT_ROW_HEIGHT = 24;
|
export const DEFAULT_ROW_HEIGHT = 24;
|
||||||
@@ -69,14 +69,15 @@ const TableBodyColumnContainer = styled.div<{
|
|||||||
multiline?: boolean;
|
multiline?: boolean;
|
||||||
justifyContent: 'left' | 'right' | 'center' | 'flex-start';
|
justifyContent: 'left' | 'right' | 'center' | 'flex-start';
|
||||||
}>((props) => ({
|
}>((props) => ({
|
||||||
display: 'flex',
|
display: 'block',
|
||||||
flexShrink: props.width === undefined ? 1 : 0,
|
flexShrink: props.width === undefined ? 1 : 0,
|
||||||
flexGrow: props.width === undefined ? 1 : 0,
|
flexGrow: props.width === undefined ? 1 : 0,
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
padding: `0 ${theme.space.small}px`,
|
padding: `0 ${theme.space.small}px`,
|
||||||
borderBottom: `1px solid ${theme.dividerColor}`,
|
borderBottom: `1px solid ${theme.dividerColor}`,
|
||||||
verticalAlign: 'top',
|
verticalAlign: 'top',
|
||||||
whiteSpace: props.multiline ? 'normal' : 'nowrap',
|
// pre-wrap preserves explicit newlines and whitespace, and wraps as well when needed
|
||||||
|
whiteSpace: props.multiline ? 'pre-wrap' : 'nowrap',
|
||||||
wordWrap: props.multiline ? 'break-word' : 'normal',
|
wordWrap: props.multiline ? 'break-word' : 'normal',
|
||||||
width: props.width,
|
width: props.width,
|
||||||
justifyContent: props.justifyContent,
|
justifyContent: props.justifyContent,
|
||||||
@@ -84,6 +85,9 @@ const TableBodyColumnContainer = styled.div<{
|
|||||||
color: 'inherit',
|
color: 'inherit',
|
||||||
backgroundColor: theme.buttonDefaultBackground,
|
backgroundColor: theme.buttonDefaultBackground,
|
||||||
},
|
},
|
||||||
|
'& p': {
|
||||||
|
margin: 0,
|
||||||
|
},
|
||||||
}));
|
}));
|
||||||
TableBodyColumnContainer.displayName = 'TableRow:TableBodyColumnContainer';
|
TableBodyColumnContainer.displayName = 'TableRow:TableBodyColumnContainer';
|
||||||
|
|
||||||
@@ -118,7 +122,7 @@ export const TableRow = memo(function TableRow({
|
|||||||
.map((col) => {
|
.map((col) => {
|
||||||
const value = (col as any).onRender
|
const value = (col as any).onRender
|
||||||
? (col as any).onRender(record)
|
? (col as any).onRender(record)
|
||||||
: normalizeCellValue((record as any)[col.key]);
|
: DataFormatter.format((record as any)[col.key], col.formatters);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TableBodyColumnContainer
|
<TableBodyColumnContainer
|
||||||
@@ -133,29 +137,3 @@ export const TableRow = memo(function TableRow({
|
|||||||
</TableBodyRowContainer>
|
</TableBodyRowContainer>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
export function normalizeCellValue(value: any): string {
|
|
||||||
switch (typeof value) {
|
|
||||||
case 'boolean':
|
|
||||||
return value ? 'true' : 'false';
|
|
||||||
case 'number':
|
|
||||||
return '' + value;
|
|
||||||
case 'undefined':
|
|
||||||
return '';
|
|
||||||
case 'string':
|
|
||||||
return value;
|
|
||||||
case 'object': {
|
|
||||||
if (value === null) return '';
|
|
||||||
if (value instanceof Date) {
|
|
||||||
return (
|
|
||||||
value.toTimeString().split(' ')[0] +
|
|
||||||
'.' +
|
|
||||||
pad('' + value.getMilliseconds(), 3)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return JSON.stringify(value, null, 2);
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
return '<unrenderable value>';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -58,12 +58,12 @@ test('update and append', async () => {
|
|||||||
class="css-1b7miqb-TableBodyRowContainer efe0za01"
|
class="css-1b7miqb-TableBodyRowContainer efe0za01"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="css-bqa56k-TableBodyColumnContainer efe0za00"
|
class="css-1xxqqu6-TableBodyColumnContainer efe0za00"
|
||||||
>
|
>
|
||||||
test DataTable
|
test DataTable
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="css-bqa56k-TableBodyColumnContainer efe0za00"
|
class="css-1xxqqu6-TableBodyColumnContainer efe0za00"
|
||||||
>
|
>
|
||||||
true
|
true
|
||||||
</div>
|
</div>
|
||||||
@@ -115,12 +115,12 @@ test('column visibility', async () => {
|
|||||||
class="css-1b7miqb-TableBodyRowContainer efe0za01"
|
class="css-1b7miqb-TableBodyRowContainer efe0za01"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="css-bqa56k-TableBodyColumnContainer efe0za00"
|
class="css-1xxqqu6-TableBodyColumnContainer efe0za00"
|
||||||
>
|
>
|
||||||
test DataTable
|
test DataTable
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="css-bqa56k-TableBodyColumnContainer efe0za00"
|
class="css-1xxqqu6-TableBodyColumnContainer efe0za00"
|
||||||
>
|
>
|
||||||
true
|
true
|
||||||
</div>
|
</div>
|
||||||
@@ -140,7 +140,7 @@ test('column visibility', async () => {
|
|||||||
class="css-1b7miqb-TableBodyRowContainer efe0za01"
|
class="css-1b7miqb-TableBodyRowContainer efe0za01"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="css-bqa56k-TableBodyColumnContainer efe0za00"
|
class="css-1xxqqu6-TableBodyColumnContainer efe0za00"
|
||||||
>
|
>
|
||||||
test DataTable
|
test DataTable
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
16
desktop/flipper-plugin/src/utils/safeStringify.tsx
Normal file
16
desktop/flipper-plugin/src/utils/safeStringify.tsx
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the MIT license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree.
|
||||||
|
*
|
||||||
|
* @format
|
||||||
|
*/
|
||||||
|
|
||||||
|
export function safeStringify(value: any) {
|
||||||
|
try {
|
||||||
|
return JSON.stringify(value, null, 2);
|
||||||
|
} catch (e) {
|
||||||
|
return '<Failed to serialize: ' + e + '>';
|
||||||
|
}
|
||||||
|
}
|
||||||
12
desktop/flipper-plugin/src/utils/urlRegex.tsx
Normal file
12
desktop/flipper-plugin/src/utils/urlRegex.tsx
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the MIT license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree.
|
||||||
|
*
|
||||||
|
* @format
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Source: https://github.com/sindresorhus/linkify-urls/blob/e0cf2a2d51dc8f5f95d93f97ecb1cc804cc9e6be/index.js#L5
|
||||||
|
// n.b. no /g flag, as that makes the regex stateful! Which is not needed for splitting
|
||||||
|
export const urlRegex = /((?<!\+)(?:https?(?::\/\/))(?:www\.)?(?:[a-zA-Z\d-_.]+(?:(?:\.|@)[a-zA-Z\d]{2,})|localhost)(?:(?:[-a-zA-Z\d:%_+.~#*$!?&//=@]*)(?:[,](?![\s]))*)*)/;
|
||||||
@@ -526,6 +526,7 @@ Layout elements can be used to organize the screen layout.
|
|||||||
See `View > Flipper Style Guide` inside the Flipper application for more details.
|
See `View > Flipper Style Guide` inside the Flipper application for more details.
|
||||||
|
|
||||||
### DataTable
|
### DataTable
|
||||||
|
### DataFormatter
|
||||||
|
|
||||||
Coming soon.
|
Coming soon.
|
||||||
|
|
||||||
|
|||||||
@@ -104,9 +104,7 @@ const App: () => React$Node = () => {
|
|||||||
fetch(API, {headers: {accept: 'application/json'}})
|
fetch(API, {headers: {accept: 'application/json'}})
|
||||||
.then((res) => res.json())
|
.then((res) => res.json())
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
console.log(
|
console.log(data.status);
|
||||||
'Got status: ' + JSON.stringify(data, null, 2),
|
|
||||||
);
|
|
||||||
setNpmStatus(data.status.description);
|
setNpmStatus(data.status.description);
|
||||||
})
|
})
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user