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
190 lines
5.1 KiB
TypeScript
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,
|
|
};
|