Files
flipper/desktop/flipper-plugin/src/ui/DataFormatter.tsx
Michel Weststrate 44521315c4 Set up separate package build
Summary:
Added a microbundle based build setup to the data-source folder to be able to package just that folder.

For simplicity / iteration speed, this is only used to publish externally. Our own code still references the source files directly.

More strict separation can be done later if there is external adoption.

Reviewed By: nikoant

Differential Revision: D28056699

fbshipit-source-id: a011b615cfffeff8ecb879bd7281a71085cea965
2021-05-10 07:03:40 -07:00

190 lines
5.1 KiB
TypeScript

/**
* 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 {
CaretRightOutlined,
CaretUpOutlined,
CopyOutlined,
} from '@ant-design/icons';
import {Button, Typography} from 'antd';
import {isPlainObject, pad} from 'lodash';
import React, {createElement, Fragment, isValidElement, useState} from 'react';
import {tryGetFlipperLibImplementation} from '../plugin/FlipperLib';
import {safeStringify} from '../utils/safeStringify';
import {urlRegex} from '../utils/urlRegex';
import {useTableRedraw} from '../data-source/index';
import {theme} from './theme';
/**
* 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, '0')
);
}
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>';
}
},
truncate(maxLength: number) {
return (value: any) => {
if (typeof value === 'string' && value.length > maxLength) {
return <TruncateHelper value={value} maxLength={maxLength} />;
}
return 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 (isValidElement(value)) {
return value;
}
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 &&
(Array.isArray(value) || isPlainObject(value))
) {
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);
},
};
// exported for testing
export function TruncateHelper({
value,
maxLength,
}: {
value: string;
maxLength: number;
}) {
const [collapsed, setCollapsed] = useState(true);
const redrawRow = useTableRedraw();
return (
<>
{collapsed ? value.substr(0, maxLength) : value}
<Button
onClick={() => {
setCollapsed((c) => !c);
redrawRow?.();
}}
size="small"
type="text"
style={truncateButtonStyle}
icon={collapsed ? <CaretRightOutlined /> : <CaretUpOutlined />}>
{collapsed ? `and ${value.length - maxLength} more` : 'collapse'}
</Button>
<Button
icon={<CopyOutlined />}
onClick={() => {
tryGetFlipperLibImplementation()?.writeTextToClipboard(value);
}}
size="small"
type="text"
style={truncateButtonStyle}>
copy
</Button>
</>
);
}
const truncateButtonStyle = {
color: theme.primaryColor,
marginLeft: 4,
};