/** * 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 {Button, Divider, Input, Modal, Typography} from 'antd'; import { DataInspector, Panel, theme, Layout, styled, useLocalStorageState, } from 'flipper-plugin'; import React, {useState} from 'react'; import { ClientNode, Color, Inspectable, InspectableObject, Metadata, } from '../../ClientTypes'; import {MetadataMap} from '../../DesktopTypes'; import {NoData} from '../sidebar/inspector/NoData'; import {css, cx} from '@emotion/css'; import {upperFirst, sortBy, omit} from 'lodash'; import {any} from 'lodash/fp'; import {InspectableColor} from '../../ClientTypes'; import {transformAny} from '../../utils/dataTransform'; import {SearchOutlined} from '@ant-design/icons'; type ModalData = { data: unknown; title: string; }; const panelCss = css` & > .ant-collapse-item .ant-collapse-header { background-color: ${theme.backgroundDefault}; padding-left: 0px; } & > .ant-collapse-item .ant-collapse-header .ant-collapse-expand-icon { width: 18px; } `; export function AttributesInspector({ node, metadata, }: { node: ClientNode; metadata: MetadataMap; }) { const [modalData, setModalData] = useState(null); const [attributeFilter, setAttributeFilter] = useLocalStorageState( 'attribute-filter', '', ); const showComplexTypeModal = (modaldata: ModalData) => { setModalData(modaldata); }; const handleCancel = () => { setModalData(null); }; const keys = Object.keys(node.attributes); const sections = keys .map((key, _) => { /** * The node top-level attributes refer to the displayable panels aka sections. * 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, showComplexTypeModal, attributeFilter, ); }) .filter((section) => section != null); if (sections.length === 0 && !attributeFilter) { return ; } return ( <> {modalData != null && ( )} setAttributeFilter(e.target.value)} placeholder="Filter attributes" prefix={} /> {sections.length === 0 ? ( ) : ( sections.concat([ , ]) )} ); } function AttributeSection( metadataMap: MetadataMap, name: string, inspectable: InspectableObject, onDisplayModal: (modaldata: ModalData) => void, attributeFilter: string, ) { const attributesOrSubSubsections = Object.entries(inspectable.fields) .map(([fieldKey, attributeValue]) => { const metadataId: number = Number(fieldKey); const attributeMetadata = metadataMap.get(metadataId); const attributeName = upperFirst(attributeMetadata?.name) ?? String(metadataId); //subsections are complex types that are only 1 level deep const isSubSection = attributeValue.type === 'object' && !any( (inspectable) => inspectable.type === 'array' || inspectable.type === 'object', Object.values(attributeValue.fields), ); return { attributeName, attributeMetadata, isSubSection, attributeValue, metadataId, }; }) .filter( ({attributeName}) => !attributeFilter || attributeName.toLowerCase().includes(attributeFilter), ); //push sub sections to the end const sortedAttributesOrSubsections = sortBy( attributesOrSubSubsections, [(item) => item.isSubSection], (item) => item.attributeName, ); const children = sortedAttributesOrSubsections .map(({isSubSection, attributeValue, attributeMetadata, attributeName}) => { if (attributeMetadata == null) { return null; } if (isSubSection) { if (attributeValue.type === 'object') { return ( ); } } return ( ); }) .filter((attr) => attr != null); if (children.length > 0) { return ( {...children} ); } else { return null; } } function SubSection({ attributeName, inspectableObject, metadataMap, onDisplayModal, }: { attributeName: string; inspectableObject: InspectableObject; metadataMap: MetadataMap; onDisplayModal: (modaldata: ModalData) => void; }) { const children = Object.entries(inspectableObject.fields).map( ([key, value]) => { const metadataId: number = Number(key); const attributeMetadata = metadataMap.get(metadataId); if (attributeMetadata == null) { return null; } const attributeName = upperFirst(attributeMetadata?.name) ?? String(metadataId); return ( ); }, ); if (children.length === 0) { return null; } return ( {attributeName} {children} ); } function NamedAttribute({ key, name, value, metadataMap, attributeMetadata, onDisplayModal, }: { name: string; value: Inspectable; attributeMetadata: Metadata; metadataMap: MetadataMap; key: string; onDisplayModal: (modaldata: ModalData) => void; }) { return ( {name} ); } /** * disables hover and focsued states */ const readOnlyInput = css` overflow: hidden; //stop random scrollbars from showing up font-size: small; :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; }) { let formatted: any = value; if (typeof value === 'number') { //cap the number of decimal places to 5 but dont add trailing zeros formatted = Number.parseFloat(value.toFixed(5)); } return ( ); } function StyledTextArea({ value, color, mutable, }: { value: any; color: string; mutable: boolean; rightAddon?: string; }) { return ( ); } const boolColor = '#C41D7F'; const stringColor = '#AF5800'; const enumColor = '#006D75'; const numberColor = '#003EB3'; type NumberGroupValue = {value: number; addonText: string}; function NumberGroup({values}: {values: NumberGroupValue[]}) { return ( {values.map(({value, addonText}, idx) => ( ))} ); } function AttributeValue({ metadataMap, name, onDisplayModal, inspectable, }: { onDisplayModal: (modaldata: ModalData) => void; attributeMetadata: Metadata; metadataMap: MetadataMap; name: string; inspectable: Inspectable; }) { switch (inspectable.type) { case 'boolean': return ( ); case 'unknown': case 'text': return ( ); case 'number': return ( ); case 'enum': return ( ); case 'size': return ( ); case 'coordinate': return ( ); case 'coordinate3d': return ( ); case 'space': return ( ); case 'bounds': return ( ); case 'color': return ; case 'array': case 'object': return ( ); } return null; } const rowHeight = 26; function ColorInspector({inspectable}: {inspectable: InspectableColor}) { return ( ); } const ColorPreview = styled.div(({background}: {background: string}) => ({ width: rowHeight, height: rowHeight, borderRadius: '8px', borderColor: theme.disabledColor, borderStyle: 'solid', boxSizing: 'border-box', borderWidth: '1px', backgroundColor: background, })); const RGBAtoHEX = (color: Color) => { const hex = (color.r | (1 << 8)).toString(16).slice(1) + (color.g | (1 << 8)).toString(16).slice(1) + (color.b | (1 << 8)).toString(16).slice(1); return '#' + hex.toUpperCase(); }; type FourItemArray = [T, T, T, T]; function TwoByTwoNumberGroup({ values, }: { values: FourItemArray; }) { return ( ); }