New sidebar design 1/n

Summary: Added basic infra basic types

Reviewed By: lblasa

Differential Revision: D50595985

fbshipit-source-id: 48ebd74bd8ccebdd8a6d69dbda344b8d831dc04f
This commit is contained in:
Luke De Feo
2023-10-26 05:24:30 -07:00
committed by Facebook GitHub Bot
parent 50b06f2efd
commit b184500d94
7 changed files with 423 additions and 8 deletions

View File

@@ -18,6 +18,7 @@ import {
ClientNode,
Metadata,
SnapshotInfo,
MetadataId,
} from './ClientTypes';
import TypedEmitter from 'typed-emitter';
@@ -26,6 +27,7 @@ export type LiveClientState = {
nodes: Map<Id, ClientNode>;
};
export type MetadataMap = Map<MetadataId, Metadata>;
export type Color = string;
export type UIState = {

View File

@@ -21,7 +21,6 @@ import {useHotkeys} from 'react-hotkeys-hook';
import {Id, Metadata, MetadataId, ClientNode} from '../ClientTypes';
import {PerfStats} from './PerfStats';
import {Visualization2D} from './visualizer/Visualization2D';
import {Inspector} from './sidebar/Inspector';
import {TreeControls} from './tree/TreeControls';
import {Button, Spin, Typography} from 'antd';
import {QueryClientProvider} from 'react-query';
@@ -30,6 +29,8 @@ import {StreamInterceptorErrorView} from './StreamInterceptorErrorView';
import {queryClient} from '../utils/reactQuery';
import {FrameworkEventsTable} from './FrameworkEventsTable';
import {Centered} from './shared/Centered';
import {SidebarV2} from './sidebarV2/SidebarV2';
import {getNode} from '../utils/map';
export function Component() {
const instance = usePlugin(plugin);
@@ -38,6 +39,7 @@ export function Component() {
const visualiserWidth = useValue(instance.uiState.visualiserWidth);
const nodes: Map<Id, ClientNode> = useValue(instance.nodes);
const metadata: Map<MetadataId, Metadata> = useValue(instance.metadata);
const selectedNodeId = useValue(instance.uiState.selectedNode);
const [showPerfStats, setShowPerfStats] = useState(false);
@@ -155,11 +157,10 @@ export function Component() {
/>
</ResizablePanel>
<DetailSidebar width={450}>
<Inspector
os={instance.os}
<SidebarV2
metadata={metadata}
nodes={nodes}
showExtra={openBottomPanelWithContent}
selectedNode={getNode(selectedNodeId?.id, nodes)}
showBottomPanel={openBottomPanelWithContent}
/>
</DetailSidebar>
</Layout.Horizontal>

View File

@@ -126,7 +126,7 @@ export const Inspector: React.FC<Props> = ({
frameworkEventMetadata={frameworkEventMetadata}
node={selectedNode}
events={selectedFrameworkEvents}
showExtra={showExtra}
showBottomPanel={showExtra}
/>
</Tab>
)}

View File

@@ -42,7 +42,7 @@ import {tracker} from '../../../utils/tracker';
type Props = {
node: ClientNode;
events: readonly FrameworkEvent[];
showExtra?: (title: string, element: ReactNode) => void;
showBottomPanel?: (title: string, element: ReactNode) => void;
frameworkEventMetadata: Map<FrameworkEventType, FrameworkEventMetadata>;
onSetViewMode: (viewMode: ViewMode) => void;
};
@@ -50,7 +50,7 @@ type Props = {
export const FrameworkEventsInspector: React.FC<Props> = ({
node,
events,
showExtra,
showBottomPanel: showExtra,
frameworkEventMetadata,
onSetViewMode,
}) => {

View File

@@ -0,0 +1,329 @@
/**
* 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 {Input, Typography} from 'antd';
import {Panel, theme, Layout} from 'flipper-plugin';
import React from 'react';
import {
ClientNode,
Inspectable,
InspectableObject,
Metadata,
} from '../../ClientTypes';
import {MetadataMap} from '../../DesktopTypes';
import {NoData} from '../sidebar/inspector/NoData';
import {css, cx} from '@emotion/css';
import {upperFirst} from 'lodash';
export function AttributesInspector({
node,
metadata,
}: {
node: ClientNode;
metadata: MetadataMap;
}) {
const keys = Object.keys(node.attributes);
const sections = keys
.map((key, _) => {
/**
* The node top-level attributes refer to the displayable panels.
* The panel name is obtained by querying the metadata.
* The inspectable contains the actual attributes belonging to each panel.
*/
const metadataId: number = Number(key);
const sectionMetadata = metadata.get(metadataId);
if (sectionMetadata == null) {
return null;
}
const sectionAttributes = node.attributes[
metadataId
] as InspectableObject;
return AttributeSection(
metadata,
sectionMetadata.name,
sectionAttributes,
);
})
.filter((section) => section != null);
if (sections.length === 0) {
return <NoData message="No data available for this element" />;
}
return <>{sections}</>;
}
function AttributeSection(
metadataMap: MetadataMap,
name: string,
inspectable: InspectableObject,
) {
const children = Object.keys(inspectable.fields)
.map((key) => {
const metadataId: number = Number(key);
const attributeMetadata = metadataMap.get(metadataId);
if (attributeMetadata == null) {
return null;
}
const attributeValue = inspectable.fields[metadataId];
const attributeName =
upperFirst(attributeMetadata?.name) ?? String(metadataId);
return (
<Layout.Horizontal key={key} gap="small">
<Typography.Text
style={{
marginTop: 3, //to center with top input when multiline
flex: '0 0 30%', //take 30% of the width
color: theme.textColorSecondary,
fontWeight: 50,
}}>
{attributeName}
</Typography.Text>
<Layout.Container style={{flex: '1 1 auto'}}>
<AttributeValue
name={attributeName}
attributeMetadata={attributeMetadata}
metadataMap={metadataMap}
inspectable={attributeValue}
level={1}
/>
</Layout.Container>
</Layout.Horizontal>
);
})
.filter((attr) => attr != null);
if (children.length > 0) {
return (
<Panel key={name} title={name}>
<Layout.Container gap="small" padv="small">
{...children}
</Layout.Container>
</Panel>
);
} else {
return null;
}
}
/**
* disables hover and focsued states
*/
const readOnlyInput = css`
:hover {
border-color: ${theme.disabledColor} !important;
}
:focus {
border-color: ${theme.disabledColor} !important;
box-shadow: none !important;
}
box-shadow: none !important;
border-color: ${theme.disabledColor} !important;
padding: 2px 4px 2px 4px;
min-height: 20px !important; //this is for text area
`;
function StyledInput({
value,
color,
mutable,
rightAddon,
}: {
value: any;
color: string;
mutable: boolean;
rightAddon?: string;
}) {
return (
<Input
size="small"
className={cx(
!mutable ? readOnlyInput : '',
css`
//set input colour when no suffix
color: ${color};
//set input colour when has suffix
.ant-input.ant-input-sm[type='text'] {
color: ${color};
}
//set colour of suffix
.ant-input.ant-input-sm[type='text'] + .ant-input-suffix {
color: ${theme.textColorSecondary};
opacity: 0.7;
}
`,
)}
bordered
readOnly={!mutable}
value={value}
suffix={rightAddon}
/>
);
}
function StyledTextArea({
value,
color,
mutable,
}: {
value: any;
color: string;
mutable: boolean;
rightAddon?: string;
}) {
return (
<Input.TextArea
autoSize
className={!mutable ? readOnlyInput : ''}
bordered
style={{color: color}}
readOnly={!mutable}
value={value}
/>
);
}
const boolColor = '#C41D7F';
const stringColor = '#AF5800';
const enumColor = '#006D75';
const numberColor = '#003EB3';
type NumberGroupValue = {value: number; addonText: string};
function NumberGroup({values}: {values: NumberGroupValue[]}) {
return (
<Layout.Horizontal gap="small">
{values.map(({value, addonText}, idx) => (
<StyledInput
key={idx}
color={numberColor}
mutable={false}
value={value}
rightAddon={addonText}
/>
))}
</Layout.Horizontal>
);
}
function AttributeValue({
inspectable,
}: {
attributeMetadata: Metadata;
metadataMap: MetadataMap;
name: string;
inspectable: Inspectable;
level: number;
}) {
switch (inspectable.type) {
case 'boolean':
return (
<StyledInput
color={boolColor}
mutable={false}
value={inspectable.value ? 'TRUE' : 'FALSE'}
/>
);
case 'text':
return (
<StyledTextArea
color={stringColor}
mutable={false}
value={inspectable.value}
/>
);
case 'number':
return (
<StyledInput
color={numberColor}
mutable={false}
value={inspectable.value}
/>
);
case 'enum':
return (
<StyledInput
color={enumColor}
mutable={false}
value={inspectable.value}
/>
);
case 'size':
return (
<NumberGroup
values={[
{value: inspectable.value.width, addonText: 'W'},
{value: inspectable.value.height, addonText: 'H'},
]}
/>
);
case 'coordinate':
return (
<NumberGroup
values={[
{value: inspectable.value.x, addonText: 'X'},
{value: inspectable.value.y, addonText: 'Y'},
]}
/>
);
case 'coordinate3d':
return (
<NumberGroup
values={[
{value: inspectable.value.x, addonText: 'X'},
{value: inspectable.value.y, addonText: 'Y'},
{value: inspectable.value.z, addonText: 'Z'},
]}
/>
);
case 'space':
return (
<Layout.Container gap="small" style={{flex: '0 1 auto'}}>
<NumberGroup
values={[
{value: inspectable.value.top, addonText: 'T'},
{value: inspectable.value.left, addonText: 'L'},
]}
/>
<NumberGroup
values={[
{value: inspectable.value.bottom, addonText: 'B'},
{value: inspectable.value.right, addonText: 'R'},
]}
/>
</Layout.Container>
);
case 'bounds':
return (
<Layout.Container gap="small" style={{flex: '0 1 auto'}}>
<NumberGroup
values={[
{value: inspectable.value.x, addonText: 'X'},
{value: inspectable.value.y, addonText: 'Y'},
]}
/>
<NumberGroup
values={[
{value: inspectable.value.width, addonText: 'W'},
{value: inspectable.value.height, addonText: 'H'},
]}
/>
</Layout.Container>
);
}
return null;
}

View File

@@ -0,0 +1,82 @@
/**
* 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 {ClientNode, MetadataId, Metadata} from '../../ClientTypes';
import {plugin} from '../../index';
import React, {ReactNode} from 'react';
import {Layout, Tab, Tabs, usePlugin, useValue} from 'flipper-plugin';
import {NoData} from '../sidebar/inspector/NoData';
import {Tooltip} from 'antd';
import {AttributesInspector} from './AttributesInspector';
import {FrameworkEventsInspector} from '../sidebar/inspector/FrameworkEventsInspector';
import {theme} from 'flipper-plugin';
// eslint-disable-next-line rulesdir/no-restricted-imports-clone
import {Glyph} from 'flipper';
type Props = {
selectedNode?: ClientNode;
metadata: Map<MetadataId, Metadata>;
showBottomPanel: (title: string, element: ReactNode) => void;
};
export function SidebarV2({selectedNode, metadata, showBottomPanel}: Props) {
const instance = usePlugin(plugin);
const frameworkEventMetadata = useValue(instance.frameworkEventMetadata);
if (!selectedNode) {
return <NoData message="Please select a node to view its details" />;
}
const selectedFrameworkEvents = selectedNode.id
? instance.frameworkEvents.getAllRecordsByIndex({nodeId: selectedNode.id})
: [];
return (
<Layout.Container gap pad>
<Tabs
localStorageKeyOverride="sidebar-tabs"
grow
centered
key={selectedNode.id}>
<Tab
tab={
<Tooltip title="Attributes">
<Layout.Horizontal center>
<Glyph name="data-table" size={16} color={theme.primaryColor} />
</Layout.Horizontal>
</Tooltip>
}>
<AttributesInspector node={selectedNode} metadata={metadata} />
</Tab>
{selectedFrameworkEvents?.length > 0 && (
<Tab
key={'events'}
tab={
<Tooltip title="Events">
<Layout.Horizontal center>
<Glyph
name="weather-thunder"
size={16}
color={theme.primaryColor}
/>
</Layout.Horizontal>
</Tooltip>
}>
<FrameworkEventsInspector
onSetViewMode={instance.uiActions.onSetViewMode}
frameworkEventMetadata={frameworkEventMetadata}
node={selectedNode}
events={selectedFrameworkEvents}
showBottomPanel={showBottomPanel}
/>
</Tab>
)}
</Tabs>
</Layout.Container>
);
}

View File

@@ -33,6 +33,7 @@
"peerDependencies": {
"@ant-design/icons": "*",
"@emotion/styled": "*",
"@emotion/css": "*",
"@types/node": "*",
"@types/react": "*",
"@types/react-dom": "*",