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:
Michel Weststrate
2020-11-12 04:13:16 -08:00
committed by Facebook GitHub Bot
parent 661bea1d5b
commit 5118727cb7
5 changed files with 171 additions and 74 deletions

View File

@@ -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

View File

@@ -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;
}

View 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>
);
}

View File

@@ -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,

View File

@@ -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';