/
githubmr
/
facebook-react
Обзор
Документация
Войти
/
githubmr
/
facebook-react
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
Аналитика
main
packages/react-devtools-shared/src/devtools/views/Components/OwnersStack.js
307 строк
8 KB
Sebastian Markbåge
[DevTools] Add Badge to Owners and sometimes stack traces (#34106)
07 авг 2025, 17:39
Не верифицирован
07 авг 2025, 17:39
738aebd
Код
Авторство
О чём код?
/** * 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. * * @flow */ import * as React from 'react'; import { Fragment, useCallback, useContext, useLayoutEffect, useReducer, useRef, useState, } from 'react'; import Button from '../Button'; import ButtonIcon from '../ButtonIcon'; import Toggle from '../Toggle'; import ElementBadges from './ElementBadges'; import {OwnersListContext, useChangeOwnerAction} from './OwnersListContext'; import {TreeDispatcherContext, TreeStateContext} from './TreeContext'; import {useIsOverflowing} from '../hooks'; import {StoreContext} from '../context'; import Tooltip from '../Components/reach-ui/tooltip'; import { Menu, MenuList, MenuButton, MenuItem, } from '../Components/reach-ui/menu-button'; import type {SerializedElement} from 'react-devtools-shared/src/frontend/types'; import styles from './OwnersStack.css'; type SelectOwner = (owner: SerializedElement | null) => void; type ACTION_UPDATE_OWNER_ID = { type: 'UPDATE_OWNER_ID', ownerID: number | null, owners: Array<SerializedElement>, }; type ACTION_UPDATE_SELECTED_INDEX = { type: 'UPDATE_SELECTED_INDEX', selectedIndex: number, }; type Action = ACTION_UPDATE_OWNER_ID | ACTION_UPDATE_SELECTED_INDEX; type State = { ownerID: number | null, owners: Array<SerializedElement>, selectedIndex: number, }; function dialogReducer(state: State, action: Action) { switch (action.type) { case 'UPDATE_OWNER_ID': const selectedIndex = action.owners.findIndex( owner => owner.id === action.ownerID, ); return { ownerID: action.ownerID, owners: action.owners, selectedIndex, }; case 'UPDATE_SELECTED_INDEX': return { ...state, selectedIndex: action.selectedIndex, }; default: throw new Error(`Invalid action "${action.type}"`); } } export default function OwnerStack(): React.Node { const read = useContext(OwnersListContext); const {ownerID} = useContext(TreeStateContext); const treeDispatch = useContext(TreeDispatcherContext); const changeOwnerAction = useChangeOwnerAction(); const [state, dispatch] = useReducer<State, State, Action>(dialogReducer, { ownerID: null, owners: [], selectedIndex: 0, }); // When an owner is selected, we either need to update the selected index, or we need to fetch a new list of owners. // We use a reducer here so that we can avoid fetching a new list unless the owner ID has actually changed. if (ownerID === null) { dispatch({ type: 'UPDATE_OWNER_ID', ownerID: null, owners: [], }); } else if (ownerID !== state.ownerID) { const isInStore = state.owners.findIndex(owner => owner.id === ownerID) >= 0; dispatch({ type: 'UPDATE_OWNER_ID', ownerID, owners: isInStore ? state.owners : read(ownerID) || [], }); } const {owners, selectedIndex} = state; const selectOwner = useCallback<SelectOwner>( (owner: SerializedElement | null) => { if (owner !== null) { const index = owners.indexOf(owner); dispatch({ type: 'UPDATE_SELECTED_INDEX', selectedIndex: index >= 0 ? index : 0, }); changeOwnerAction(owner.id); } else { dispatch({ type: 'UPDATE_SELECTED_INDEX', selectedIndex: 0, }); treeDispatch({type: 'RESET_OWNER_STACK'}); } }, [owners, treeDispatch], ); const [elementsTotalWidth, setElementsTotalWidth] = useState(0); const elementsBarRef = useRef<HTMLDivElement | null>(null); const isOverflowing = useIsOverflowing(elementsBarRef, elementsTotalWidth); const selectedOwner = owners[selectedIndex]; useLayoutEffect(() => { // If we're already overflowing, then we don't need to re-measure items. // That's because once the owners stack is open, it can only get larger (by drilling in). // A totally new stack can only be reached by exiting this mode and re-entering it. if (elementsBarRef.current === null || isOverflowing) { return () => {}; } let totalWidth = 0; for (let i = 0; i < owners.length; i++) { const element = elementsBarRef.current.children[i]; const computedStyle = getComputedStyle(element); totalWidth += element.offsetWidth + parseInt(computedStyle.marginLeft, 10) + parseInt(computedStyle.marginRight, 10); } setElementsTotalWidth(totalWidth); }, [elementsBarRef, isOverflowing, owners.length]); return ( <div className={styles.OwnerStack}> <div className={styles.Bar} ref={elementsBarRef}> {isOverflowing && ( <Fragment> <ElementsDropdown owners={owners} selectedIndex={selectedIndex} selectOwner={selectOwner} /> <BackToOwnerButton owners={owners} selectedIndex={selectedIndex} selectOwner={selectOwner} /> {selectedOwner != null && ( <ElementView owner={selectedOwner} isSelected={true} selectOwner={selectOwner} /> )} </Fragment> )} {!isOverflowing && owners.map((owner, index) => ( <ElementView key={index} owner={owner} isSelected={index === selectedIndex} selectOwner={selectOwner} /> ))} </div> <div className={styles.VRule} /> <Button onClick={() => selectOwner(null)} title="Back to tree view"> <ButtonIcon type="close" /> </Button> </div> ); } type ElementsDropdownProps = { owners: Array<SerializedElement>, selectedIndex: number, selectOwner: SelectOwner, }; function ElementsDropdown({owners, selectOwner}: ElementsDropdownProps) { const store = useContext(StoreContext); const menuItems = []; for (let index = owners.length - 1; index >= 0; index--) { const owner = owners[index]; const isInStore = store.containsElement(owner.id); menuItems.push( <MenuItem key={owner.id} className={`${styles.Component} ${isInStore ? '' : styles.NotInStore}`} onSelect={() => (isInStore ? selectOwner(owner) : null)}> {owner.displayName} <ElementBadges hocDisplayNames={owner.hocDisplayNames} environmentName={owner.env} compiledWithForget={owner.compiledWithForget} className={styles.BadgesBlock} /> </MenuItem>, ); } return ( <Menu> <MenuButton className={styles.MenuButton}> <Tooltip label="Open elements dropdown"> <span className={styles.MenuButtonContent} tabIndex={-1}> <ButtonIcon type="more" /> </span> </Tooltip> </MenuButton> <MenuList className={styles.Modal}>{menuItems}</MenuList> </Menu> ); } type ElementViewProps = { isSelected: boolean, owner: SerializedElement, selectOwner: SelectOwner, ... }; function ElementView({isSelected, owner, selectOwner}: ElementViewProps) { const store = useContext(StoreContext); const {displayName, hocDisplayNames, compiledWithForget} = owner; const isInStore = store.containsElement(owner.id); const handleChange = useCallback(() => { if (isInStore) { selectOwner(owner); } }, [isInStore, selectOwner, owner]); return ( <Toggle className={`${styles.Component} ${isInStore ? '' : styles.NotInStore}`} isChecked={isSelected} onChange={handleChange}> {displayName} <ElementBadges hocDisplayNames={hocDisplayNames} environmentName={owner.env} compiledWithForget={compiledWithForget} className={styles.BadgesBlock} /> </Toggle> ); } type BackToOwnerButtonProps = { owners: Array<SerializedElement>, selectedIndex: number, selectOwner: SelectOwner, }; function BackToOwnerButton({ owners, selectedIndex, selectOwner, }: BackToOwnerButtonProps) { const store = useContext(StoreContext); if (selectedIndex <= 0) { return null; } const owner = owners[selectedIndex - 1]; const isInStore = store.containsElement(owner.id); return ( <Button className={isInStore ? undefined : styles.NotInStore} onClick={() => (isInStore ? selectOwner(owner) : null)} title={`Up to ${owner.displayName || 'owner'}`}> <ButtonIcon type="previous" /> </Button> ); }