Add bookmarks section to AppInspect
Summary: This diff adds support for the navigation plugin bookmarks to the appinspect tab. Support for path discovery, and path params will be added in a next diff. Features: * click a bookmark and navigate to it * sync bookmark state and uri with navigation plugin * manually enter a path and navigate to it by using <ENTER> Reviewed By: cekkaewnumchai Differential Revision: D24620250 fbshipit-source-id: 14b393a5456b4afeef69444d2120c8f01686e602
This commit is contained in:
committed by
Facebook GitHub Bot
parent
661bea1d5b
commit
5118727cb7
@@ -15,7 +15,6 @@ import {useStore} from '../utils/useStore';
|
|||||||
import {useMemoize} from '../utils/useMemoize';
|
import {useMemoize} from '../utils/useMemoize';
|
||||||
import {State} from '../reducers';
|
import {State} from '../reducers';
|
||||||
|
|
||||||
// TODO T71355623
|
|
||||||
// eslint-disable-next-line flipper/no-relative-imports-across-packages
|
// eslint-disable-next-line flipper/no-relative-imports-across-packages
|
||||||
import type {NavigationPlugin} from '../../../plugins/navigation/index';
|
import type {NavigationPlugin} from '../../../plugins/navigation/index';
|
||||||
// eslint-disable-next-line flipper/no-relative-imports-across-packages
|
// eslint-disable-next-line flipper/no-relative-imports-across-packages
|
||||||
|
|||||||
@@ -11,12 +11,18 @@ import React from 'react';
|
|||||||
import {Alert} from 'antd';
|
import {Alert} from 'antd';
|
||||||
import {LeftSidebar, SidebarTitle, InfoIcon} from '../LeftSidebar';
|
import {LeftSidebar, SidebarTitle, InfoIcon} from '../LeftSidebar';
|
||||||
import {Layout, Link, styled} from '../../ui';
|
import {Layout, Link, styled} from '../../ui';
|
||||||
import {NUX, theme} from 'flipper-plugin';
|
import {theme} from 'flipper-plugin';
|
||||||
import {AppSelector} from './AppSelector';
|
import {AppSelector} from './AppSelector';
|
||||||
import {useStore} from '../../utils/useStore';
|
import {useStore} from '../../utils/useStore';
|
||||||
import {PluginList} from './PluginList';
|
import {PluginList} from './PluginList';
|
||||||
import ScreenCaptureButtons from '../../chrome/ScreenCaptureButtons';
|
import ScreenCaptureButtons from '../../chrome/ScreenCaptureButtons';
|
||||||
import MetroButton from '../../chrome/MetroButton';
|
import MetroButton from '../../chrome/MetroButton';
|
||||||
|
import {BookmarkSection} from './BookmarkSection';
|
||||||
|
import {useMemoize} from '../../utils/useMemoize';
|
||||||
|
import Client from '../../Client';
|
||||||
|
import {State} from '../../reducers';
|
||||||
|
import BaseDevice from '../../devices/BaseDevice';
|
||||||
|
import MetroDevice from '../../devices/MetroDevice';
|
||||||
|
|
||||||
const appTooltip = (
|
const appTooltip = (
|
||||||
<>
|
<>
|
||||||
@@ -30,8 +36,23 @@ const appTooltip = (
|
|||||||
);
|
);
|
||||||
|
|
||||||
export function AppInspect() {
|
export function AppInspect() {
|
||||||
const selectedDevice = useStore((state) => state.connections.selectedDevice);
|
const connections = useStore((state) => state.connections);
|
||||||
const isArchived = !!selectedDevice?.isArchived;
|
|
||||||
|
const metroDevice = useMemoize(findMetroDevice, [connections.devices]);
|
||||||
|
const client = useMemoize(findBestClient, [
|
||||||
|
connections.clients,
|
||||||
|
connections.selectedApp,
|
||||||
|
connections.userPreferredApp,
|
||||||
|
]);
|
||||||
|
// // if the selected device is Metro, we want to keep the owner of the selected App as active device if possible
|
||||||
|
const activeDevice = useMemoize(findBestDevice, [
|
||||||
|
client,
|
||||||
|
connections.devices,
|
||||||
|
connections.selectedDevice,
|
||||||
|
metroDevice,
|
||||||
|
connections.userPreferredDevice,
|
||||||
|
]);
|
||||||
|
const isArchived = !!activeDevice?.isArchived;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<LeftSidebar>
|
<LeftSidebar>
|
||||||
@@ -42,14 +63,14 @@ export function AppInspect() {
|
|||||||
</SidebarTitle>
|
</SidebarTitle>
|
||||||
<Layout.Container padv="small" padh="medium" gap={theme.space.large}>
|
<Layout.Container padv="small" padh="medium" gap={theme.space.large}>
|
||||||
<AppSelector />
|
<AppSelector />
|
||||||
{
|
{isArchived ? (
|
||||||
isArchived ? (
|
<Alert
|
||||||
<Alert
|
message="This device is a snapshot and cannot be interacted with."
|
||||||
message="This device is a snapshot and cannot be interacted with."
|
type="info"
|
||||||
type="info"
|
/>
|
||||||
/>
|
) : (
|
||||||
) : null /* TODO: add bookmarks back T77016599 */
|
<BookmarkSection />
|
||||||
}
|
)}
|
||||||
{!isArchived && (
|
{!isArchived && (
|
||||||
<Toolbar gap>
|
<Toolbar gap>
|
||||||
<MetroButton useSandy />
|
<MetroButton useSandy />
|
||||||
@@ -59,8 +80,12 @@ export function AppInspect() {
|
|||||||
</Layout.Container>
|
</Layout.Container>
|
||||||
</Layout.Container>
|
</Layout.Container>
|
||||||
<Layout.ScrollContainer vertical padv={theme.space.large}>
|
<Layout.ScrollContainer vertical padv={theme.space.large}>
|
||||||
{selectedDevice ? (
|
{activeDevice ? (
|
||||||
<PluginList />
|
<PluginList
|
||||||
|
activeDevice={activeDevice}
|
||||||
|
metroDevice={metroDevice}
|
||||||
|
client={client}
|
||||||
|
/>
|
||||||
) : (
|
) : (
|
||||||
<Alert message="No device or app selected." type="info" />
|
<Alert message="No device or app selected." type="info" />
|
||||||
)}
|
)}
|
||||||
@@ -75,3 +100,44 @@ const Toolbar = styled(Layout.Horizontal)({
|
|||||||
border: 'none',
|
border: 'none',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export function findBestClient(
|
||||||
|
clients: Client[],
|
||||||
|
selectedApp: string | null,
|
||||||
|
userPreferredApp: string | null,
|
||||||
|
): Client | undefined {
|
||||||
|
return clients.find((c) => c.id === (selectedApp || userPreferredApp));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function findMetroDevice(
|
||||||
|
devices: State['connections']['devices'],
|
||||||
|
): MetroDevice | undefined {
|
||||||
|
return devices?.find(
|
||||||
|
(device) => device.os === 'Metro' && !device.isArchived,
|
||||||
|
) as MetroDevice;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function findBestDevice(
|
||||||
|
client: Client | undefined,
|
||||||
|
devices: State['connections']['devices'],
|
||||||
|
selectedDevice: BaseDevice | null,
|
||||||
|
metroDevice: BaseDevice | undefined,
|
||||||
|
userPreferredDevice: string | null,
|
||||||
|
): BaseDevice | undefined {
|
||||||
|
// if not Metro device, use the selected device as metro device
|
||||||
|
const selected = selectedDevice ?? undefined;
|
||||||
|
if (selected !== metroDevice) {
|
||||||
|
return selected;
|
||||||
|
}
|
||||||
|
// if there is an active app, use device owning the app
|
||||||
|
if (client) {
|
||||||
|
return client.deviceSync;
|
||||||
|
}
|
||||||
|
// if no active app, use the preferred device
|
||||||
|
if (userPreferredDevice) {
|
||||||
|
return (
|
||||||
|
devices.find((device) => device.title === userPreferredDevice) ?? selected
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return selected;
|
||||||
|
}
|
||||||
|
|||||||
80
desktop/app/src/sandy-chrome/appinspect/BookmarkSection.tsx
Normal file
80
desktop/app/src/sandy-chrome/appinspect/BookmarkSection.tsx
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
/**
|
||||||
|
* 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 React, {useCallback, useMemo} from 'react';
|
||||||
|
import {AutoComplete, Input} from 'antd';
|
||||||
|
import {StarFilled, StarOutlined} from '@ant-design/icons';
|
||||||
|
import {useStore} from '../../utils/useStore';
|
||||||
|
import {NUX, useValue} from 'flipper-plugin';
|
||||||
|
import {navPluginStateSelector} from '../../chrome/LocationsButton';
|
||||||
|
|
||||||
|
// eslint-disable-next-line flipper/no-relative-imports-across-packages
|
||||||
|
import type {NavigationPlugin} from '../../../../plugins/navigation/index';
|
||||||
|
|
||||||
|
export function BookmarkSection() {
|
||||||
|
const navPlugin = useStore(navPluginStateSelector);
|
||||||
|
|
||||||
|
return navPlugin ? (
|
||||||
|
<NUX
|
||||||
|
title="Use bookmarks to directly navigate to a location in the app."
|
||||||
|
placement="right">
|
||||||
|
<BookmarkSectionInput navPlugin={navPlugin} />
|
||||||
|
</NUX>
|
||||||
|
) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function BookmarkSectionInput({navPlugin}: {navPlugin: NavigationPlugin}) {
|
||||||
|
const currentURI = useValue(navPlugin.currentURI);
|
||||||
|
const bookmarks = useValue(navPlugin.bookmarks);
|
||||||
|
|
||||||
|
const isBookmarked = useMemo(() => bookmarks.has(currentURI), [
|
||||||
|
bookmarks,
|
||||||
|
currentURI,
|
||||||
|
]);
|
||||||
|
const handleBookmarkClick = useCallback(() => {
|
||||||
|
if (isBookmarked) {
|
||||||
|
navPlugin.removeBookmark(currentURI);
|
||||||
|
} else {
|
||||||
|
navPlugin.addBookmark({
|
||||||
|
uri: currentURI,
|
||||||
|
commonName: null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [navPlugin, currentURI, isBookmarked]);
|
||||||
|
|
||||||
|
const bookmarkButton = isBookmarked ? (
|
||||||
|
<StarFilled onClick={handleBookmarkClick} />
|
||||||
|
) : (
|
||||||
|
<StarOutlined onClick={handleBookmarkClick} />
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AutoComplete
|
||||||
|
value={currentURI}
|
||||||
|
onSelect={navPlugin.navigateTo}
|
||||||
|
options={Array.from(bookmarks.values()).map((bookmark) => ({
|
||||||
|
value: bookmark.uri,
|
||||||
|
label: bookmark.commonName
|
||||||
|
? `${bookmark.commonName} - ${bookmark.uri}`
|
||||||
|
: bookmark.uri,
|
||||||
|
}))}>
|
||||||
|
<Input
|
||||||
|
addonAfter={bookmarkButton}
|
||||||
|
defaultValue="<select a bookmark>"
|
||||||
|
value={currentURI}
|
||||||
|
onChange={(e) => {
|
||||||
|
navPlugin.currentURI.set(e.target.value);
|
||||||
|
}}
|
||||||
|
onPressEnter={(e) => {
|
||||||
|
navPlugin.navigateTo(currentURI);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</AutoComplete>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -23,29 +23,24 @@ import BaseDevice from '../../devices/BaseDevice';
|
|||||||
import {getFavoritePlugins} from '../../chrome/mainsidebar/sidebarUtils';
|
import {getFavoritePlugins} from '../../chrome/mainsidebar/sidebarUtils';
|
||||||
import {PluginDetails} from 'flipper-plugin-lib';
|
import {PluginDetails} from 'flipper-plugin-lib';
|
||||||
import {useMemoize} from '../../utils/useMemoize';
|
import {useMemoize} from '../../utils/useMemoize';
|
||||||
|
import MetroDevice from '../../devices/MetroDevice';
|
||||||
|
|
||||||
const {SubMenu} = Menu;
|
const {SubMenu} = Menu;
|
||||||
const {Text} = Typography;
|
const {Text} = Typography;
|
||||||
|
|
||||||
export const PluginList = memo(function PluginList() {
|
export const PluginList = memo(function PluginList({
|
||||||
|
client,
|
||||||
|
activeDevice,
|
||||||
|
metroDevice,
|
||||||
|
}: {
|
||||||
|
client: Client | undefined;
|
||||||
|
activeDevice: BaseDevice | undefined;
|
||||||
|
metroDevice: MetroDevice | undefined;
|
||||||
|
}) {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const connections = useStore((state) => state.connections);
|
const connections = useStore((state) => state.connections);
|
||||||
const plugins = useStore((state) => state.plugins);
|
const plugins = useStore((state) => state.plugins);
|
||||||
|
|
||||||
const metroDevice = useMemoize(findMetroDevice, [connections.devices]);
|
|
||||||
const client = useMemoize(findBestClient, [
|
|
||||||
connections.clients,
|
|
||||||
connections.selectedApp,
|
|
||||||
connections.userPreferredApp,
|
|
||||||
]);
|
|
||||||
// // if the selected device is Metro, we want to keep the owner of the selected App as active device if possible
|
|
||||||
const activeDevice = useMemoize(findBestDevice, [
|
|
||||||
client,
|
|
||||||
connections.devices,
|
|
||||||
connections.selectedDevice,
|
|
||||||
metroDevice,
|
|
||||||
connections.userPreferredDevice,
|
|
||||||
]);
|
|
||||||
const {
|
const {
|
||||||
devicePlugins,
|
devicePlugins,
|
||||||
metroPlugins,
|
metroPlugins,
|
||||||
@@ -351,45 +346,6 @@ function getPluginTooltip(details: PluginDetails): string {
|
|||||||
}`;
|
}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function findBestClient(
|
|
||||||
clients: Client[],
|
|
||||||
selectedApp: string | null,
|
|
||||||
userPreferredApp: string | null,
|
|
||||||
): Client | undefined {
|
|
||||||
return clients.find((c) => c.id === (selectedApp || userPreferredApp));
|
|
||||||
}
|
|
||||||
|
|
||||||
export function findMetroDevice(
|
|
||||||
devices: State['connections']['devices'],
|
|
||||||
): BaseDevice | undefined {
|
|
||||||
return devices?.find((device) => device.os === 'Metro' && !device.isArchived);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function findBestDevice(
|
|
||||||
client: Client | undefined,
|
|
||||||
devices: State['connections']['devices'],
|
|
||||||
selectedDevice: BaseDevice | null,
|
|
||||||
metroDevice: BaseDevice | undefined,
|
|
||||||
userPreferredDevice: string | null,
|
|
||||||
): BaseDevice | undefined {
|
|
||||||
// if not Metro device, use the selected device as metro device
|
|
||||||
const selected = selectedDevice ?? undefined;
|
|
||||||
if (selected !== metroDevice) {
|
|
||||||
return selected;
|
|
||||||
}
|
|
||||||
// if there is an active app, use device owning the app
|
|
||||||
if (client) {
|
|
||||||
return client.deviceSync;
|
|
||||||
}
|
|
||||||
// if no active app, use the preferred device
|
|
||||||
if (userPreferredDevice) {
|
|
||||||
return (
|
|
||||||
devices.find((device) => device.title === userPreferredDevice) ?? selected
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return selected;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function computePluginLists(
|
export function computePluginLists(
|
||||||
device: BaseDevice | undefined,
|
device: BaseDevice | undefined,
|
||||||
metroDevice: BaseDevice | undefined,
|
metroDevice: BaseDevice | undefined,
|
||||||
|
|||||||
@@ -11,12 +11,8 @@ import {
|
|||||||
createMockFlipperWithPlugin,
|
createMockFlipperWithPlugin,
|
||||||
MockFlipperResult,
|
MockFlipperResult,
|
||||||
} from '../../../test-utils/createMockFlipperWithPlugin';
|
} from '../../../test-utils/createMockFlipperWithPlugin';
|
||||||
import {
|
import {computePluginLists} from '../PluginList';
|
||||||
findBestClient,
|
import {findBestClient, findBestDevice, findMetroDevice} from '../AppInspect';
|
||||||
findBestDevice,
|
|
||||||
findMetroDevice,
|
|
||||||
computePluginLists,
|
|
||||||
} from '../PluginList';
|
|
||||||
import {FlipperPlugin} from '../../../plugin';
|
import {FlipperPlugin} from '../../../plugin';
|
||||||
import MetroDevice from '../../../devices/MetroDevice';
|
import MetroDevice from '../../../devices/MetroDevice';
|
||||||
import BaseDevice from '../../../devices/BaseDevice';
|
import BaseDevice from '../../../devices/BaseDevice';
|
||||||
|
|||||||
Reference in New Issue
Block a user