Files
flipper/desktop/flipper-plugin/src/ui/data-inspector/DataDescription.tsx
Michel Weststrate 39be769bab DataInspector renames
Summary:
Two minor renames:
* `DataInspector` => `DataInspectorNode`
* `ManagedDataInspector` => `DataInspector`

This aligns the internal and public name of the component, and better captures the meaning of the original `DataInspector` class.

The diff looks quite hefty, but that seems to be a phabricator issue caused by the filename swap; barely a thing changed :)

Reviewed By: jknoxville

Differential Revision: D28028554

fbshipit-source-id: d3d61fcb50abffaeae4bd1d26966604cece37b03
2021-04-28 06:33:04 -07:00

714 lines
17 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 {Typography, Popover, Input, Select, Checkbox} from 'antd';
import {DataInspectorSetValue} from './DataInspectorNode';
import {PureComponent} from 'react';
import styled from '@emotion/styled';
import {SketchPicker, CompactPicker} from 'react-color';
import React, {KeyboardEvent} from 'react';
import {HighlightContext} from '../Highlight';
import {parseColor} from '../../utils/parseColor';
import {TimelineDataDescription} from './TimelineDataDescription';
import {theme} from '../theme';
import {EditOutlined} from '@ant-design/icons';
import type {CheckboxChangeEvent} from 'antd/lib/checkbox';
const {Link} = Typography;
// Based on FIG UI Core, TODO: does that still makes sense?
export const presetColors = Object.values({
blue: '#4267b2', // Blue - Active-state nav glyphs, nav bars, links, buttons
green: '#42b72a', // Green - Confirmation, success, commerce and status
red: '#FC3A4B', // Red - Badges, error states
blueGrey: '#5f6673', // Blue Grey
slate: '#b9cad2', // Slate
aluminum: '#a3cedf', // Aluminum
seaFoam: '#54c7ec', // Sea Foam
teal: '#6bcebb', // Teal
lime: '#a3ce71', // Lime
lemon: '#fcd872', // Lemon
orange: '#f7923b', // Orange
tomato: '#fb724b', // Tomato - Tometo? Tomato.
cherry: '#f35369', // Cherry
pink: '#ec7ebd', // Pink
grape: '#8c72cb', // Grape
});
const NullValue = styled.span({
color: 'rgb(128, 128, 128)',
});
NullValue.displayName = 'DataDescription:NullValue';
const UndefinedValue = styled.span({
color: 'rgb(128, 128, 128)',
});
UndefinedValue.displayName = 'DataDescription:UndefinedValue';
const StringValue = styled.span({
color: '#e04c60',
wordWrap: 'break-word',
});
StringValue.displayName = 'DataDescription:StringValue';
const ColorValue = styled.span({
color: '#5f6673',
});
ColorValue.displayName = 'DataDescription:ColorValue';
const SymbolValue = styled.span({
color: 'rgb(196, 26, 22)',
});
SymbolValue.displayName = 'DataDescription:SymbolValue';
const NumberValue = styled.span({
color: '#4dbba6',
});
NumberValue.displayName = 'DataDescription:NumberValue';
const ColorBox = styled.span<{color: string}>((props) => ({
backgroundColor: props.color,
boxShadow: 'inset 0 0 1px rgba(0, 0, 0, 1)',
display: 'inline-block',
height: 12,
marginRight: 4,
verticalAlign: 'middle',
width: 12,
borderRadius: 4,
}));
ColorBox.displayName = 'DataDescription:ColorBox';
const FunctionKeyword = styled.span({
color: 'rgb(170, 13, 145)',
fontStyle: 'italic',
});
FunctionKeyword.displayName = 'DataDescription:FunctionKeyword';
const FunctionName = styled.span({
fontStyle: 'italic',
});
FunctionName.displayName = 'DataDescription:FunctionName';
const ColorPickerDescription = styled.div({
display: 'inline',
position: 'relative',
});
ColorPickerDescription.displayName = 'DataDescription:ColorPickerDescription';
const EmptyObjectValue = styled.span({
fontStyle: 'italic',
});
EmptyObjectValue.displayName = 'DataDescription:EmptyObjectValue';
export type DataDescriptionType =
| 'number'
| 'string'
| 'boolean'
| 'undefined'
| 'null'
| 'object'
| 'array'
| 'date'
| 'symbol'
| 'function'
| 'bigint'
| 'text' // deprecated, please use string
| 'enum' // unformatted string
| 'color'
| 'picker' // multiple choise item like an, eehhh, enum
| 'timeline'
| 'color_lite'; // color with limited palette, specific for fblite;
type DataDescriptionProps = {
path?: Array<string>;
type: DataDescriptionType;
value: any;
extra?: any;
setValue: DataInspectorSetValue | null | undefined;
};
type DescriptionCommitOptions = {
value: any;
keep: boolean;
clear: boolean;
set: boolean;
};
class NumberTextEditor extends PureComponent<{
commit: (opts: DescriptionCommitOptions) => void;
type: DataDescriptionType;
value: any;
origValue: any;
}> {
onNumberTextInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const val =
this.props.type === 'number'
? parseFloat(e.target.value)
: e.target.value;
this.props.commit({
clear: false,
keep: true,
value: val,
set: false,
});
};
onNumberTextInputKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Enter') {
const val =
this.props.type === 'number'
? parseFloat(this.props.value)
: this.props.value;
this.props.commit({clear: true, keep: true, value: val, set: true});
} else if (e.key === 'Escape') {
this.props.commit({
clear: true,
keep: false,
value: this.props.origValue,
set: false,
});
}
};
onNumberTextRef = (ref: HTMLElement | undefined | null) => {
if (ref) {
ref.focus();
}
};
onNumberTextBlur = () => {
this.props.commit({
clear: true,
keep: true,
value: this.props.value,
set: true,
});
};
render() {
const extraProps: any = {};
if (this.props.type === 'number') {
// render as a HTML number input
extraProps.type = 'number';
// step="any" allows any sort of float to be input, otherwise we're limited
// to decimal
extraProps.step = 'any';
}
return (
<Input
key="input"
{...extraProps}
size="small"
onChange={this.onNumberTextInputChange}
onKeyDown={this.onNumberTextInputKeyDown}
ref={this.onNumberTextRef}
onBlur={this.onNumberTextBlur}
value={this.props.value}
style={{fontSize: 11}}
/>
);
}
}
type DataDescriptionState = {
editing: boolean;
origValue: any;
value: any;
};
export class DataDescription extends PureComponent<
DataDescriptionProps,
DataDescriptionState
> {
constructor(props: DataDescriptionProps, context: Object) {
super(props, context);
this.state = {
editing: false,
origValue: '',
value: '',
};
}
commit = (opts: DescriptionCommitOptions) => {
const {path, setValue} = this.props;
if (opts.keep && setValue && path) {
const val = opts.value;
this.setState({value: val});
if (opts.set) {
setValue(path, val);
}
}
if (opts.clear) {
this.setState({
editing: false,
origValue: '',
value: '',
});
}
};
_renderEditing() {
const {type, extra} = this.props;
const {origValue, value} = this.state;
if (
type === 'string' ||
type === 'text' ||
type === 'number' ||
type === 'enum'
) {
return (
<NumberTextEditor
type={type}
value={value}
origValue={origValue}
commit={this.commit}
/>
);
}
if (type === 'color') {
return <ColorEditor value={value} commit={this.commit} />;
}
if (type === 'color_lite') {
return (
<ColorEditor
value={value}
colorSet={extra.colorSet}
commit={this.commit}
/>
);
}
return null;
}
_hasEditUI() {
const {type} = this.props;
return (
type === 'string' ||
type === 'text' ||
type === 'number' ||
type === 'enum' ||
type === 'color' ||
type === 'color_lite'
);
}
onEditStart = () => {
this.setState({
editing: this._hasEditUI(),
origValue: this.props.value,
value: this.props.value,
});
};
render(): any {
if (this.state.editing) {
return this._renderEditing();
} else {
return (
<DataDescriptionPreview
type={this.props.type}
value={this.props.value}
extra={this.props.extra}
editable={!!this.props.setValue}
commit={this.commit}
onEdit={this.onEditStart}
/>
);
}
}
}
class ColorEditor extends PureComponent<{
value: any;
colorSet?: Array<string | number>;
commit: (opts: DescriptionCommitOptions) => void;
}> {
onBlur = (newVisibility: boolean) => {
if (!newVisibility) {
this.props.commit({
clear: true,
keep: false,
value: this.props.value,
set: true,
});
}
};
onChange = ({
hex,
rgb: {a, b, g, r},
}: {
hex: string;
rgb: {a: number; b: number; g: number; r: number};
}) => {
const prev = this.props.value;
let val;
if (typeof prev === 'string') {
if (a === 1) {
// hex is fine and has an implicit 100% alpha
val = hex;
} else {
// turn into a css rgba value
val = `rgba(${r}, ${g}, ${b}, ${a})`;
}
} else if (typeof prev === 'number') {
// compute RRGGBBAA value
val = (Math.round(a * 255) & 0xff) << 24;
val |= (r & 0xff) << 16;
val |= (g & 0xff) << 8;
val |= b & 0xff;
const prevClear = ((prev >> 24) & 0xff) === 0;
const onlyAlphaChanged = (prev & 0x00ffffff) === (val & 0x00ffffff);
if (!onlyAlphaChanged && prevClear) {
val = 0xff000000 | (val & 0x00ffffff);
}
} else {
return;
}
this.props.commit({clear: false, keep: true, value: val, set: true});
};
onChangeLite = ({
rgb: {a, b, g, r},
}: {
rgb: {a: number; b: number; g: number; r: number};
}) => {
const prev = this.props.value;
if (typeof prev !== 'number') {
return;
}
// compute RRGGBBAA value
let val = (Math.round(a * 255) & 0xff) << 24;
val |= (r & 0xff) << 16;
val |= (g & 0xff) << 8;
val |= b & 0xff;
this.props.commit({clear: false, keep: true, value: val, set: true});
};
render() {
const colorInfo = parseColor(this.props.value);
if (!colorInfo) {
return null;
}
return (
<Popover
trigger={'click'}
onVisibleChange={this.onBlur}
content={() =>
this.props.colorSet ? (
<CompactPicker
color={colorInfo}
colors={this.props.colorSet
.filter((x) => x != 0)
.map(parseColor)
.map((rgba) => {
if (!rgba) {
return '';
}
return `rgba(${rgba.r}, ${rgba.g}, ${rgba.b}, ${rgba.a})`;
})}
onChange={(color: {
hex: string;
hsl: {
a?: number;
h: number;
l: number;
s: number;
};
rgb: {a?: number; b: number; g: number; r: number};
}) => {
this.onChangeLite({rgb: {...color.rgb, a: color.rgb.a || 0}});
}}
/>
) : (
<SketchPicker
color={colorInfo}
presetColors={presetColors}
onChange={(color: {
hex: string;
hsl: {
a?: number;
h: number;
l: number;
s: number;
};
rgb: {a?: number; b: number; g: number; r: number};
}) => {
this.onChange({
hex: color.hex,
rgb: {...color.rgb, a: color.rgb.a || 1},
});
}}
/>
)
}>
<ColorPickerDescription>
<DataDescriptionPreview
type="color"
value={this.props.value}
extra={this.props.colorSet}
editable={false}
commit={this.props.commit}
/>
</ColorPickerDescription>
</Popover>
);
}
}
class DataDescriptionPreview extends PureComponent<{
type: DataDescriptionType;
value: any;
extra?: any;
editable: boolean;
commit: (opts: DescriptionCommitOptions) => void;
onEdit?: () => void;
}> {
onClick = () => {
const {onEdit} = this.props;
if (this.props.editable && onEdit) {
onEdit();
}
};
render() {
const {type, value} = this.props;
const description = (
<DataDescriptionContainer
type={type}
value={value}
editable={this.props.editable}
commit={this.props.commit}
/>
);
// booleans are always editable so don't require the onEditStart handler
if (type === 'boolean') {
return description;
}
return (
<span onClick={this.onClick} role="button" tabIndex={-1}>
{description}
</span>
);
}
}
type Picker = {
values: Set<string>;
selected: string;
};
class DataDescriptionContainer extends PureComponent<{
type: DataDescriptionType;
value: any;
editable: boolean;
commit: (opts: DescriptionCommitOptions) => void;
}> {
static contextType = HighlightContext; // Replace with useHighlighter
context!: React.ContextType<typeof HighlightContext>;
onChangeCheckbox = (e: CheckboxChangeEvent) => {
this.props.commit({
clear: true,
keep: true,
value: e.target.checked,
set: true,
});
};
render(): any {
const {type, editable, value: val} = this.props;
const highlighter = this.context;
switch (type) {
case 'timeline': {
return (
<>
<TimelineDataDescription
canSetCurrent={editable}
timeline={JSON.parse(val)}
onClick={(id) => {
this.props.commit({
value: id,
keep: true,
clear: false,
set: true,
});
}}
/>
</>
);
}
case 'number':
return <NumberValue>{+val}</NumberValue>;
case 'color': {
const colorInfo = parseColor(val);
if (typeof val === 'number' && val === 0) {
return <UndefinedValue>(not set)</UndefinedValue>;
} else if (colorInfo) {
const {a, b, g, r} = colorInfo;
return (
<>
<ColorBox
key="color-box"
color={`rgba(${r}, ${g}, ${b}, ${a})`}
/>
<ColorValue key="value">
rgba({r}, {g}, {b}, {a === 1 ? '1' : a.toFixed(2)})
</ColorValue>
</>
);
} else {
return <span>Malformed color</span>;
}
}
case 'color_lite': {
const colorInfo = parseColor(val);
if (typeof val === 'number' && val === 0) {
return <UndefinedValue>(not set)</UndefinedValue>;
} else if (colorInfo) {
const {a, b, g, r} = colorInfo;
return (
<>
<ColorBox
key="color-box"
color={`rgba(${r}, ${g}, ${b}, ${a})`}
/>
<ColorValue key="value">
rgba({r}, {g}, {b}, {a === 1 ? '1' : a.toFixed(2)})
</ColorValue>
</>
);
} else {
return <span>Malformed color</span>;
}
}
case 'picker': {
const picker: Picker = JSON.parse(val);
return (
<Select
disabled={!this.props.editable}
options={Array.from(picker.values).map((value) => ({
value,
label: value,
}))}
value={picker.selected}
onChange={(value: string) =>
this.props.commit({
value,
keep: true,
clear: false,
set: true,
})
}
size="small"
style={{fontSize: 11}}
/>
);
}
case 'text':
case 'string':
const isUrl = val.startsWith('http://') || val.startsWith('https://');
if (isUrl) {
return (
<>
<Link href={val}>{highlighter.render(val)}</Link>
{editable && (
<EditOutlined
style={{
color: theme.disabledColor,
cursor: 'pointer',
marginLeft: 8,
}}
/>
)}
</>
);
} else {
return (
<StringValue>{highlighter.render(`"${val || ''}"`)}</StringValue>
);
}
case 'enum':
return <StringValue>{highlighter.render(val)}</StringValue>;
case 'boolean':
return editable ? (
<Checkbox
checked={!!val}
disabled={!editable}
onChange={this.onChangeCheckbox}
/>
) : (
<StringValue>{'' + val}</StringValue>
);
case 'undefined':
return <UndefinedValue>undefined</UndefinedValue>;
case 'date':
if (Object.prototype.toString.call(val) === '[object Date]') {
return <span>{val.toString()}</span>;
} else {
return <span>{val}</span>;
}
case 'null':
return <NullValue>null</NullValue>;
// no description necessary as we'll typically wrap it in [] or {} which
// already denotes the type
case 'array':
return val.length <= 0 ? <EmptyObjectValue>[]</EmptyObjectValue> : null;
case 'object':
return Object.keys(val ?? {}).length <= 0 ? (
<EmptyObjectValue>{'{}'}</EmptyObjectValue>
) : null;
case 'function':
return (
<span>
<FunctionKeyword>function</FunctionKeyword>
<FunctionName>&nbsp;{val.name}()</FunctionName>
</span>
);
case 'symbol':
return <SymbolValue>Symbol()</SymbolValue>;
default:
return <span>Unknown type "{type}"</span>;
}
}
}