Restore TimeMachine in Flipper

Summary:
The goal is to restore the *TimeMachine* functionality in Flipper while the new UI Debugger is working in a revamped version. This version was previously removed in D42573698 (b31f8c8755).

This time machine functionality will only be available for *LithoView* that are backed by a *ComponentTree*.

Below you can find a high-level overview of the current schema:

{F884011465}

Reviewed By: LukeDefeo, adityasharat

Differential Revision: D43574130

fbshipit-source-id: fe9d661e5fcce9655e70be4785652e7048dade54
This commit is contained in:
Fabio Carballo
2023-02-27 12:19:37 -08:00
committed by Facebook GitHub Bot
parent 7d99913416
commit fa74ebba68
3 changed files with 196 additions and 0 deletions

View File

@@ -9,7 +9,14 @@ package com.facebook.flipper.plugins.inspector;
import com.facebook.flipper.core.FlipperObject; import com.facebook.flipper.core.FlipperObject;
import com.facebook.flipper.core.FlipperValue; import com.facebook.flipper.core.FlipperValue;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Set; import java.util.Set;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
public class InspectorValue<T> implements FlipperValue { public class InspectorValue<T> implements FlipperValue {
@@ -30,6 +37,7 @@ public class InspectorValue<T> implements FlipperValue {
public static final Type<String> Enum = new Type<>("enum"); public static final Type<String> Enum = new Type<>("enum");
public static final Type<Integer> Color = new Type<>("color"); public static final Type<Integer> Color = new Type<>("color");
public static final Type<Picker> Picker = new Type<>("picker"); public static final Type<Picker> Picker = new Type<>("picker");
public static final Type<Timeline> Timeline = new Type<>("timeline");
private final String mName; private final String mName;
@@ -108,4 +116,84 @@ public class InspectorValue<T> implements FlipperValue {
return b.toString(); return b.toString();
} }
} }
/**
* A widget that represents a timeline. Each point has a moment to be placed on the timeline, and
* a key to be identified as. The current field represents the key of the point in the timeline
* that matches the current moment in time.
*/
public static final class Timeline {
public final List<TimePoint> time;
public final String current;
public Timeline(List<TimePoint> time, String current) {
Collections.sort(
time,
new Comparator<TimePoint>() {
@Override
public int compare(TimePoint stringTimePointEntry, TimePoint t1) {
return Float.compare(stringTimePointEntry.moment, t1.moment);
}
});
this.time = time;
this.current = current;
}
private JSONObject toJson() {
final JSONArray points = new JSONArray();
for (TimePoint value : time) {
points.put(value.toJson());
}
try {
return new JSONObject().put("time", points).put("current", current);
} catch (JSONException t) {
throw new RuntimeException(t);
}
}
@Override
public String toString() {
return toJson().toString();
}
/**
* An entry in the timeline, identified by its key. They're sorted in Flipper by moment, and are
* rendered according to the display and color. Any additional properties attached to the point
* will be displayed when it's selected.
*/
public static final class TimePoint {
public final long moment;
public final String display;
public final String color;
public final String key;
public final Map<String, String> properties;
public TimePoint(
String key, long moment, String display, String color, Map<String, String> properties) {
this.key = key;
this.moment = moment;
this.display = display;
this.color = color;
this.properties = properties;
}
private JSONObject toJson() {
try {
return new JSONObject()
.put("moment", moment)
.put("display", display)
.put("color", color)
.put("key", key)
.put("properties", new JSONObject(properties));
} catch (JSONException t) {
throw new RuntimeException(t);
}
}
@Override
public String toString() {
return toJson().toString();
}
}
}
} }

View File

@@ -15,6 +15,7 @@ import {SketchPicker, CompactPicker} from 'react-color';
import React, {KeyboardEvent} from 'react'; import React, {KeyboardEvent} from 'react';
import {HighlightContext} from '../Highlight'; import {HighlightContext} from '../Highlight';
import {parseColor} from '../../utils/parseColor'; import {parseColor} from '../../utils/parseColor';
import {TimelineDataDescription} from './TimelineDataDescription';
import {theme} from '../theme'; import {theme} from '../theme';
import {EditOutlined} from '@ant-design/icons'; import {EditOutlined} from '@ant-design/icons';
// This import is OK since it is a type-only import // This import is OK since it is a type-only import
@@ -551,6 +552,25 @@ class DataDescriptionContainer extends PureComponent<{
const highlighter = this.context; const highlighter = this.context;
switch (type) { 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': case 'number':
return <NumberValue>{+val}</NumberValue>; return <NumberValue>{+val}</NumberValue>;

View File

@@ -0,0 +1,88 @@
/**
* Copyright (c) Meta Platforms, Inc. and 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 {Component, ReactNode} from 'react';
import React from 'react';
import {MarkerTimeline} from '../MarkerTimeline';
import {Button} from 'antd';
import {presetColors} from './DataDescription';
import {DataInspector} from './DataInspector';
type TimePoint = {
moment: number;
display: string;
color: string;
key: string;
properties: {[key: string]: string};
};
type Timeline = {
time: TimePoint[];
current: string;
};
type Props = {
canSetCurrent?: boolean;
timeline: Timeline;
onClick: (selected: string) => void;
};
type State = {
selected: string;
};
export class TimelineDataDescription extends Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = {selected: props.timeline.current};
}
render(): ReactNode {
const moments = Object.values(this.props.timeline.time);
const firstMoment = moments[0].moment;
const points = moments.map((value) => ({
label: value.display,
time: value.moment - firstMoment,
color:
Object.entries(presetColors).find(([k, _]) => k === value.color)?.[1] ??
value.color,
key: value.key,
}));
return (
<>
{this.props.canSetCurrent && (
<div>
<Button
onClick={() => this.props.onClick(this.state.selected)}
disabled={this.state.selected === this.props.timeline.current}>
Set as current
</Button>
</div>
)}
<div>
<MarkerTimeline
points={points}
onClick={(ids) => this.setState({selected: ids[0]})}
maxGap={50}
selected={this.state.selected}
/>
</div>
<div>
<DataInspector
data={
this.props.timeline.time.find(
(value) => value.key === this.state.selected,
)?.properties ?? {}
}
/>
</div>
</>
);
}
}