Yarn workspaces
Summary: 1) moved "sonar/desktop/src" to "sonar/desktop/app/src", so "app" is now a separate package containing the core Flipper app code 2) Configured yarn workspaces with the root in "sonar/desktop": app, static, pkg, doctor, headless-tests. Plugins are not included for now, I plan to do this later. Reviewed By: jknoxville Differential Revision: D20535782 fbshipit-source-id: 600b2301960f37c7d72166e0d04eba462bec9fc1
This commit is contained in:
committed by
Facebook GitHub Bot
parent
676d7bbd24
commit
863f89351e
665
desktop/app/src/ui/components/data-inspector/DataDescription.tsx
Normal file
665
desktop/app/src/ui/components/data-inspector/DataDescription.tsx
Normal file
@@ -0,0 +1,665 @@
|
||||
/**
|
||||
* 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 Link from '../Link';
|
||||
import {DataInspectorSetValue} from './DataInspector';
|
||||
import {PureComponent} from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
import {SketchPicker, CompactPicker} from 'react-color';
|
||||
import {Component, Fragment} from 'react';
|
||||
import Popover from '../Popover';
|
||||
import {colors} from '../colors';
|
||||
import Input from '../Input';
|
||||
import React, {KeyboardEvent} from 'react';
|
||||
import Glyph from '../Glyph';
|
||||
|
||||
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: colors.cherryDark1,
|
||||
wordWrap: 'break-word',
|
||||
});
|
||||
StringValue.displayName = 'DataDescription:StringValue';
|
||||
|
||||
const ColorValue = styled.span({
|
||||
color: colors.blueGrey,
|
||||
});
|
||||
ColorValue.displayName = 'DataDescription:ColorValue';
|
||||
|
||||
const SymbolValue = styled.span({
|
||||
color: 'rgb(196, 26, 22)',
|
||||
});
|
||||
SymbolValue.displayName = 'DataDescription:SymbolValue';
|
||||
|
||||
const NumberValue = styled.span({
|
||||
color: colors.tealDark1,
|
||||
});
|
||||
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: 5,
|
||||
verticalAlign: 'middle',
|
||||
width: 12,
|
||||
}));
|
||||
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';
|
||||
|
||||
type DataDescriptionProps = {
|
||||
path?: Array<string>;
|
||||
type: string;
|
||||
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: string;
|
||||
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}
|
||||
compact={true}
|
||||
onChange={this.onNumberTextInputChange}
|
||||
onKeyDown={this.onNumberTextInputKeyDown}
|
||||
ref={this.onNumberTextRef}
|
||||
onBlur={this.onNumberTextBlur}
|
||||
value={this.props.value}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
type DataDescriptionState = {
|
||||
editing: boolean;
|
||||
origValue: any;
|
||||
value: any;
|
||||
};
|
||||
|
||||
export default 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={Boolean(this.props.setValue)}
|
||||
commit={this.commit}
|
||||
onEdit={this.onEditStart}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ColorEditor extends Component<{
|
||||
value: any;
|
||||
colorSet?: Array<string | number>;
|
||||
commit: (opts: DescriptionCommitOptions) => void;
|
||||
}> {
|
||||
onBlur = () => {
|
||||
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 <Fragment />;
|
||||
}
|
||||
|
||||
return (
|
||||
<ColorPickerDescription>
|
||||
<DataDescriptionPreview
|
||||
type="color"
|
||||
value={this.props.value}
|
||||
extra={this.props.colorSet}
|
||||
editable={false}
|
||||
commit={this.props.commit}
|
||||
/>
|
||||
<Popover onDismiss={this.onBlur}>
|
||||
{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={[
|
||||
colors.blue,
|
||||
colors.green,
|
||||
colors.red,
|
||||
colors.blueGrey,
|
||||
colors.slate,
|
||||
colors.aluminum,
|
||||
colors.seaFoam,
|
||||
colors.teal,
|
||||
colors.lime,
|
||||
colors.lemon,
|
||||
colors.orange,
|
||||
colors.tomato,
|
||||
colors.cherry,
|
||||
colors.pink,
|
||||
colors.grape,
|
||||
]}
|
||||
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},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Popover>
|
||||
</ColorPickerDescription>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class DataDescriptionPreview extends Component<{
|
||||
type: string;
|
||||
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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function parseColor(
|
||||
val: string | number,
|
||||
):
|
||||
| {
|
||||
r: number;
|
||||
g: number;
|
||||
b: number;
|
||||
a: number;
|
||||
}
|
||||
| undefined
|
||||
| null {
|
||||
if (typeof val === 'number') {
|
||||
const a = ((val >> 24) & 0xff) / 255;
|
||||
const r = (val >> 16) & 0xff;
|
||||
const g = (val >> 8) & 0xff;
|
||||
const b = val & 0xff;
|
||||
return {a, b, g, r};
|
||||
}
|
||||
if (typeof val !== 'string') {
|
||||
return;
|
||||
}
|
||||
if (val[0] !== '#') {
|
||||
return;
|
||||
}
|
||||
|
||||
// remove leading hash
|
||||
val = val.slice(1);
|
||||
|
||||
// only allow RGB and ARGB hex values
|
||||
if (val.length !== 3 && val.length !== 6 && val.length !== 8) {
|
||||
return;
|
||||
}
|
||||
|
||||
// split every 2 characters
|
||||
const parts = val.match(/.{1,2}/g);
|
||||
if (!parts) {
|
||||
return;
|
||||
}
|
||||
|
||||
// get the alpha value
|
||||
let a = 1;
|
||||
|
||||
// extract alpha if passed AARRGGBB
|
||||
if (val.length === 8) {
|
||||
a = parseInt(parts.shift() || '0', 16) / 255;
|
||||
}
|
||||
|
||||
const size = val.length;
|
||||
const [r, g, b] = parts.map(num => {
|
||||
if (size === 3) {
|
||||
return parseInt(num + num, 16);
|
||||
} else {
|
||||
return parseInt(num, 16);
|
||||
}
|
||||
});
|
||||
|
||||
return {a, b, g, r};
|
||||
}
|
||||
|
||||
class DataDescriptionContainer extends Component<{
|
||||
type: string;
|
||||
value: any;
|
||||
editable: boolean;
|
||||
commit: (opts: DescriptionCommitOptions) => void;
|
||||
}> {
|
||||
onChangeCheckbox = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
this.props.commit({
|
||||
clear: true,
|
||||
keep: true,
|
||||
value: e.target.checked,
|
||||
set: true,
|
||||
});
|
||||
};
|
||||
|
||||
render(): any {
|
||||
const {type, editable, value: val} = this.props;
|
||||
|
||||
switch (type) {
|
||||
case 'number':
|
||||
return <NumberValue>{Number(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 'text':
|
||||
case 'string':
|
||||
if (val.startsWith('http://') || val.startsWith('https://')) {
|
||||
return (
|
||||
<>
|
||||
<Link href={val}>{val}</Link>
|
||||
<Glyph
|
||||
name="pencil"
|
||||
variant="outline"
|
||||
color={colors.light20}
|
||||
size={16}
|
||||
style={{cursor: 'pointer', marginLeft: 8}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
} else {
|
||||
return <StringValue>"{String(val || '')}"</StringValue>;
|
||||
}
|
||||
|
||||
case 'enum':
|
||||
return <StringValue>{String(val)}</StringValue>;
|
||||
|
||||
case 'boolean':
|
||||
return editable ? (
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={Boolean(val)}
|
||||
disabled={!editable}
|
||||
onChange={this.onChangeCheckbox}
|
||||
/>
|
||||
) : (
|
||||
<StringValue>{String(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>;
|
||||
|
||||
case 'array':
|
||||
case 'object':
|
||||
// no description necessary as we'll typically wrap it in [] or {} which already denotes the
|
||||
// type
|
||||
return null;
|
||||
|
||||
case 'function':
|
||||
return (
|
||||
<span>
|
||||
<FunctionKeyword>function</FunctionKeyword>
|
||||
<FunctionName> {val.name}()</FunctionName>
|
||||
</span>
|
||||
);
|
||||
|
||||
case 'symbol':
|
||||
return <SymbolValue>Symbol()</SymbolValue>;
|
||||
|
||||
default:
|
||||
return <span>Unknown type "{type}"</span>;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user