Move app/src (mostly) to flipper-ui-core/src

Summary:
This diff moves all UI code from app/src to app/flipper-ui-core. That is now slightly too much (e.g. node deps are not removed yet), but from here it should be easier to move things out again, as I don't want this diff to be open for too long to avoid too much merge conflicts.

* But at least flipper-ui-core is Electron free :)
* Killed all cross module imports as well, as they where now even more in the way
* Some unit test needed some changes, most not too big (but emotion hashes got renumbered in the snapshots, feel free to ignore that)
* Found some files that were actually meaningless (tsconfig in plugins, WatchTools files, that start generating compile errors, removed those

Follow up work:
* make flipper-ui-core configurable, and wire up flipper-server-core in Electron instead of here
* remove node deps (aigoncharov)
* figure out correct place to load GKs, plugins, make intern requests etc., and move to the correct module
* clean up deps

Reviewed By: aigoncharov

Differential Revision: D32427722

fbshipit-source-id: 14fe92e1ceb15b9dcf7bece367c8ab92df927a70
This commit is contained in:
Michel Weststrate
2021-11-16 05:25:40 -08:00
committed by Facebook GitHub Bot
parent 54b7ce9308
commit 7e50c0466a
293 changed files with 483 additions and 497 deletions

View File

@@ -0,0 +1,249 @@
/**
* 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 {PluginDetails} from 'flipper-plugin-lib';
import {Layout} from 'flipper-plugin';
import Client from '../../Client';
import {TableBodyRow} from '../../ui/components/table/types';
import React, {Component} from 'react';
import {connect} from 'react-redux';
import {Text, ManagedTable, styled, colors} from '../../ui';
import StatusIndicator from '../../ui/components/StatusIndicator';
import {State as Store} from '../../reducers';
import {PluginDefinition} from '../../plugin';
const InfoText = styled(Text)({
lineHeight: '130%',
marginBottom: 8,
});
const Ellipsis = styled(Text)({
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
});
const TableContainer = styled.div({
marginTop: 10,
height: 480,
});
const Lamp = (props: {on: boolean}) => (
<StatusIndicator statusColor={props.on ? colors.lime : colors.red} />
);
type StateFromProps = {
gatekeepedPlugins: Array<PluginDetails>;
disabledPlugins: Array<PluginDetails>;
failedPlugins: Array<[PluginDetails, string]>;
clients: Map<string, Client>;
selectedDevice: string | null | undefined;
devicePlugins: PluginDefinition[];
clientPlugins: PluginDefinition[];
};
type DispatchFromProps = {};
type OwnProps = {};
const COLUMNS = {
lamp: {
value: '',
},
name: {
value: 'Name',
},
version: {
value: 'Version',
},
status: {
value: 'Status',
},
gk: {
value: 'GK',
},
clients: {
value: 'Supported by',
},
source: {
value: 'Source',
},
};
const COLUMNS_SIZES = {
lamp: 20,
name: 'flex',
version: 60,
status: 110,
gk: 120,
clients: 90,
source: 140,
};
type Props = OwnProps & StateFromProps & DispatchFromProps;
class PluginDebugger extends Component<Props> {
buildRow(
name: string,
version: string,
loaded: boolean,
status: string,
GKname: string | null | undefined,
pluginPath: string,
): TableBodyRow {
return {
key: name.toLowerCase(),
columns: {
lamp: {value: <Lamp on={loaded} />},
name: {value: <Ellipsis>{name}</Ellipsis>},
version: {value: <Ellipsis>{version}</Ellipsis>},
status: {
value: status ? <Ellipsis title={status}>{status}</Ellipsis> : null,
},
gk: {
value: GKname && (
<Ellipsis code title={GKname}>
{GKname}
</Ellipsis>
),
},
clients: {
value: this.getSupportedClients(name),
},
source: {
value: (
<Ellipsis code title={pluginPath}>
{pluginPath}
</Ellipsis>
),
},
},
};
}
getSupportedClients(id: string): string {
return Array.from(this.props.clients.values())
.reduce((acc: Array<string>, cv: Client) => {
if (cv.plugins.has(id)) {
acc.push(cv.query.app);
}
return acc;
}, [])
.join(', ');
}
getRows(): Array<TableBodyRow> {
const rows: Array<TableBodyRow> = [];
const externalPluginPath = (p: any) => (p.isBundled ? 'bundled' : p.entry);
this.props.gatekeepedPlugins.forEach((plugin) =>
rows.push(
this.buildRow(
plugin.name,
plugin.version,
false,
'GK disabled',
plugin.gatekeeper,
externalPluginPath(plugin),
),
),
);
this.props.devicePlugins.forEach((plugin) =>
rows.push(
this.buildRow(
plugin.id,
plugin.version,
true,
'',
plugin.gatekeeper,
externalPluginPath(plugin),
),
),
);
this.props.clientPlugins.forEach((plugin) =>
rows.push(
this.buildRow(
plugin.id,
plugin.version,
true,
'',
plugin.gatekeeper,
externalPluginPath(plugin),
),
),
);
this.props.disabledPlugins.forEach((plugin) =>
rows.push(
this.buildRow(
plugin.name,
plugin.version,
false,
'disabled',
null,
externalPluginPath(plugin),
),
),
);
this.props.failedPlugins.forEach(([plugin, status]) =>
rows.push(
this.buildRow(
plugin.name,
plugin.version,
false,
status,
null,
externalPluginPath(plugin),
),
),
);
return rows.sort((a, b) => (a.key < b.key ? -1 : 1));
}
render() {
return (
<Layout.Container pad>
<InfoText>The table lists all plugins known to Flipper.</InfoText>
<TableContainer>
<ManagedTable
columns={COLUMNS}
rows={this.getRows()}
highlightableRows={false}
columnSizes={COLUMNS_SIZES}
/>
</TableContainer>
</Layout.Container>
);
}
}
export default connect<StateFromProps, DispatchFromProps, OwnProps, Store>(
({
plugins: {
devicePlugins,
clientPlugins,
gatekeepedPlugins,
disabledPlugins,
failedPlugins,
},
connections: {clients, selectedDevice},
}) => ({
devicePlugins: Array.from(devicePlugins.values()),
clientPlugins: Array.from(clientPlugins.values()),
gatekeepedPlugins,
clients,
disabledPlugins,
failedPlugins,
selectedDevice: selectedDevice && selectedDevice.serial,
}),
)(PluginDebugger);

View File

@@ -0,0 +1,331 @@
/**
* 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 {Layout, theme} from 'flipper-plugin';
import {LoadingIndicator, TableRows, ManagedTable, Glyph} from '../../ui';
import React, {useCallback, useState, useEffect} from 'react';
import {reportPlatformFailures, reportUsage} from 'flipper-common';
import reloadFlipper from '../../utils/reloadFlipper';
import {registerInstalledPlugins} from '../../reducers/plugins';
import {
UpdateResult,
getInstalledPlugins,
getUpdatablePlugins,
removePlugin,
UpdatablePluginDetails,
InstalledPluginDetails,
} from 'flipper-plugin-lib';
import {installPluginFromNpm} from 'flipper-plugin-lib';
import {State as AppState} from '../../reducers';
import {connect} from 'react-redux';
import {Dispatch, Action} from 'redux';
import PluginPackageInstaller from './PluginPackageInstaller';
import {Toolbar} from 'flipper-plugin';
import {Alert, Button, Input, Tooltip, Typography} from 'antd';
const {Text, Link} = Typography;
const TAG = 'PluginInstaller';
const columnSizes = {
name: '25%',
version: '10%',
description: 'flex',
install: '15%',
};
const columns = {
name: {
value: 'Name',
},
version: {
value: 'Version',
},
description: {
value: 'Description',
},
install: {
value: '',
},
};
type PropsFromState = {
installedPlugins: Map<string, InstalledPluginDetails>;
};
type DispatchFromProps = {
refreshInstalledPlugins: () => void;
};
type OwnProps = {
autoHeight: boolean;
};
type Props = OwnProps & PropsFromState & DispatchFromProps;
const defaultProps: OwnProps = {
autoHeight: false,
};
const PluginInstaller = function ({
refreshInstalledPlugins,
installedPlugins,
autoHeight,
}: Props) {
const [restartRequired, setRestartRequired] = useState(false);
const [query, setQuery] = useState('');
const onInstall = useCallback(async () => {
refreshInstalledPlugins();
setRestartRequired(true);
}, [refreshInstalledPlugins]);
const rows = useNPMSearch(query, onInstall, installedPlugins);
const restartApp = useCallback(() => {
reloadFlipper();
}, []);
return (
<Layout.Container gap height={500}>
{restartRequired && (
<Alert
onClick={restartApp}
type="error"
message="To apply the changes, Flipper needs to reload. Click here to reload!"
style={{cursor: 'pointer'}}
/>
)}
<Toolbar>
<Input.Search
onChange={(e) => setQuery(e.target.value)}
value={query}
placeholder="Search Flipper plugins..."
/>
</Toolbar>
<ManagedTable
rowLineHeight={28}
floating={false}
multiline
columnSizes={columnSizes}
columns={columns}
highlightableRows={false}
highlightedRows={new Set()}
autoHeight={autoHeight}
rows={rows}
horizontallyScrollable
/>
<PluginPackageInstaller onInstall={onInstall} />
</Layout.Container>
);
};
function InstallButton(props: {
name: string;
version: string;
onInstall: () => void;
updateStatus: UpdateResult;
}) {
type InstallAction =
| {kind: 'Install'; error?: string}
| {kind: 'Waiting'}
| {kind: 'Remove'; error?: string}
| {kind: 'Update'; error?: string};
const catchError =
(actionKind: 'Install' | 'Remove' | 'Update', fn: () => Promise<void>) =>
async () => {
try {
await fn();
} catch (err) {
console.error(
`Installation process of kind ${actionKind} failed with:`,
err,
);
setAction({kind: actionKind, error: err.toString()});
}
};
const mkInstallCallback = (action: 'Install' | 'Update') =>
catchError(action, async () => {
reportUsage(
action === 'Install' ? `${TAG}:install` : `${TAG}:update`,
undefined,
props.name,
);
setAction({kind: 'Waiting'});
await installPluginFromNpm(props.name);
props.onInstall();
setAction({kind: 'Remove'});
});
const performInstall = useCallback(mkInstallCallback('Install'), [
props.name,
props.version,
]);
const performUpdate = useCallback(mkInstallCallback('Update'), [
props.name,
props.version,
]);
const performRemove = useCallback(
catchError('Remove', async () => {
reportUsage(`${TAG}:remove`, undefined, props.name);
setAction({kind: 'Waiting'});
await removePlugin(props.name);
props.onInstall();
setAction({kind: 'Install'});
}),
[props.name],
);
const [action, setAction] = useState<InstallAction>(
props.updateStatus.kind === 'update-available'
? {kind: 'Update'}
: props.updateStatus.kind === 'not-installed'
? {kind: 'Install'}
: {kind: 'Remove'},
);
if (action.kind === 'Waiting') {
return <LoadingIndicator size={16} />;
}
if ((action.kind === 'Install' || action.kind === 'Remove') && action.error) {
}
const button = (
<Button
size="small"
type={action.kind !== 'Remove' ? 'primary' : undefined}
onClick={() => {
switch (action.kind) {
case 'Install':
reportPlatformFailures(performInstall(), `${TAG}:install`);
break;
case 'Remove':
reportPlatformFailures(performRemove(), `${TAG}:remove`);
break;
case 'Update':
reportPlatformFailures(performUpdate(), `${TAG}:update`);
break;
}
}}>
{action.kind}
</Button>
);
if (action.error) {
const glyph = (
<Glyph color={theme.warningColor} size={16} name="caution-triangle" />
);
return (
<Layout.Horizontal gap>
<Tooltip
placement="leftBottom"
title={`Something went wrong: ${action.error}`}
children={glyph}
/>
{button}
</Layout.Horizontal>
);
} else {
return button;
}
}
function useNPMSearch(
query: string,
onInstall: () => void,
installedPlugins: Map<string, InstalledPluginDetails>,
): TableRows {
useEffect(() => {
reportUsage(`${TAG}:open`);
}, []);
const [searchResults, setSearchResults] = useState<UpdatablePluginDetails[]>(
[],
);
const createRow = useCallback(
(h: UpdatablePluginDetails) => ({
key: h.name,
columns: {
name: {
value: <Text ellipsis>{h.name.replace(/^flipper-plugin-/, '')}</Text>,
},
version: {
value: <Text ellipsis>{h.version}</Text>,
align: 'flex-end' as 'flex-end',
},
description: {
value: (
<Layout.Horizontal center gap>
<Text ellipsis>{h.description}</Text>
<Link href={`https://yarnpkg.com/en/package/${h.name}`}>
<Glyph
color={theme.textColorActive}
name="info-circle"
size={16}
/>
</Link>
</Layout.Horizontal>
),
},
install: {
value: (
<InstallButton
name={h.name}
version={h.version}
onInstall={onInstall}
updateStatus={h.updateStatus}
/>
),
align: 'center' as 'center',
},
},
}),
[onInstall],
);
useEffect(() => {
(async () => {
let canceled = false;
const updatablePlugins = await reportPlatformFailures(
getUpdatablePlugins(query),
`${TAG}:queryIndex`,
);
if (canceled) {
return;
}
setSearchResults(updatablePlugins);
// Clean up: if query changes while we're searching, abandon results.
return () => {
canceled = true;
};
})();
}, [query, installedPlugins]);
const rows = searchResults.map(createRow);
return rows;
}
PluginInstaller.defaultProps = defaultProps;
export default connect<PropsFromState, DispatchFromProps, OwnProps, AppState>(
({plugins: {installedPlugins}}) => ({
installedPlugins,
}),
(dispatch: Dispatch<Action<any>>) => ({
refreshInstalledPlugins: async () => {
const plugins = await getInstalledPlugins();
dispatch(registerInstalledPlugins(plugins));
},
}),
)(PluginInstaller);

View File

@@ -0,0 +1,29 @@
/**
* 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 from 'react';
import {Tab, Tabs} from 'flipper-plugin';
import PluginDebugger from './PluginDebugger';
import PluginInstaller from './PluginInstaller';
import {Modal} from 'antd';
export default function (props: {onHide: () => any}) {
return (
<Modal width={800} visible onCancel={props.onHide} footer={null}>
<Tabs>
<Tab tab="Plugin Status">
<PluginDebugger />
</Tab>
<Tab tab="Install Plugins">
<PluginInstaller autoHeight />
</Tab>
</Tabs>
</Modal>
);
}

View File

@@ -0,0 +1,110 @@
/**
* 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 {
Button,
FlexRow,
Tooltip,
Glyph,
colors,
LoadingIndicator,
} from '../../ui';
import styled from '@emotion/styled';
import {default as FileSelector} from '../../ui/components/FileSelector';
import React, {useState} from 'react';
import {installPluginFromFile} from 'flipper-plugin-lib';
import {Toolbar} from 'flipper-plugin';
const CenteredGlyph = styled(Glyph)({
margin: 'auto',
marginLeft: 2,
});
const Spinner = styled(LoadingIndicator)({
margin: 'auto',
marginLeft: 16,
});
const ButtonContainer = styled(FlexRow)({
width: 76,
});
const ErrorGlyphContainer = styled(FlexRow)({
width: 20,
});
export default function PluginPackageInstaller({
onInstall,
}: {
onInstall: () => Promise<void>;
}) {
const [path, setPath] = useState('');
const [isPathValid, setIsPathValid] = useState(false);
const [error, setError] = useState<Error>();
const [inProgress, setInProgress] = useState(false);
const onClick = async () => {
setError(undefined);
setInProgress(true);
try {
await installPluginFromFile(path);
await onInstall();
} catch (e) {
setError(e);
console.error('PluginPackageInstaller install error:', e);
} finally {
setInProgress(false);
}
};
const button = inProgress ? (
<Spinner size={16} />
) : (
<Button
compact
type="primary"
disabled={!isPathValid}
title={
isPathValid
? 'Click to install the specified plugin package'
: 'Cannot install plugin package by the specified path'
}
onClick={onClick}>
Install
</Button>
);
return (
<Toolbar>
<FileSelector
placeholderText="Specify path to a Flipper package or just drag and drop it here..."
onPathChanged={(e) => {
setPath(e.path);
setIsPathValid(e.isValid);
setError(undefined);
}}
/>
<ButtonContainer>
<FlexRow>
{button}
<ErrorGlyphContainer>
{error && (
<Tooltip
options={{position: 'toRight'}}
title={`Something went wrong: ${error}`}>
<CenteredGlyph
color={colors.orange}
size={16}
name="caution-triangle"
/>
</Tooltip>
)}
</ErrorGlyphContainer>
</FlexRow>
</ButtonContainer>
</Toolbar>
);
}

View File

@@ -0,0 +1,117 @@
/**
* 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
*/
jest.mock('flipper-plugin-lib');
import {default as PluginInstaller} from '../PluginInstaller';
import React from 'react';
import {render, waitFor} from '@testing-library/react';
import configureStore from 'redux-mock-store';
import {Provider} from 'react-redux';
import type {PluginDetails} from 'flipper-plugin-lib';
import {getUpdatablePlugins, UpdatablePluginDetails} from 'flipper-plugin-lib';
import {Store} from '../../../reducers';
import {mocked} from 'ts-jest/utils';
const getUpdatablePluginsMock = mocked(getUpdatablePlugins);
function getStore(installedPlugins: PluginDetails[] = []): Store {
return configureStore([])({
application: {sessionId: 'mysession'},
plugins: {installedPlugins},
}) as Store;
}
const samplePluginDetails1: UpdatablePluginDetails = {
name: 'flipper-plugin-hello',
entry: './test/index.js',
version: '0.1.0',
specVersion: 2,
pluginType: 'client',
main: 'dist/bundle.js',
dir: '/Users/mock/.flipper/thirdparty/flipper-plugin-sample1',
source: 'src/index.js',
id: 'Hello',
title: 'Hello',
description: 'World?',
isBundled: false,
isActivatable: true,
updateStatus: {
kind: 'not-installed',
version: '0.1.0',
},
};
const samplePluginDetails2: UpdatablePluginDetails = {
name: 'flipper-plugin-world',
entry: './test/index.js',
version: '0.2.0',
specVersion: 2,
pluginType: 'client',
main: 'dist/bundle.js',
dir: '/Users/mock/.flipper/thirdparty/flipper-plugin-sample2',
source: 'src/index.js',
id: 'World',
title: 'World',
description: 'Hello?',
isBundled: false,
isActivatable: true,
updateStatus: {
kind: 'not-installed',
version: '0.2.0',
},
};
const SEARCH_RESULTS = [samplePluginDetails1, samplePluginDetails2];
afterEach(() => {
getUpdatablePluginsMock.mockClear();
});
test('load PluginInstaller list', async () => {
getUpdatablePluginsMock.mockReturnValue(Promise.resolve(SEARCH_RESULTS));
const component = (
<Provider store={getStore()}>
<PluginInstaller
// Bit ugly to have this as an effectively test-only option, but
// without, we rely on height information from Electron which we don't
// have, causing no items to be rendered.
autoHeight
/>
</Provider>
);
const {container, getByText} = render(component);
await waitFor(() => getByText('hello'));
expect(getUpdatablePluginsMock.mock.calls.length).toBe(1);
expect(container).toMatchSnapshot();
});
test('load PluginInstaller list with one plugin installed', async () => {
getUpdatablePluginsMock.mockReturnValue(
Promise.resolve([
{...samplePluginDetails1, updateStatus: {kind: 'up-to-date'}},
samplePluginDetails2,
]),
);
const store = getStore([samplePluginDetails1]);
const component = (
<Provider store={store}>
<PluginInstaller
// Bit ugly to have this as an effectively test-only option, but
// without, we rely on height information from Electron which we don't
// have, causing no items to be rendered.
autoHeight
/>
</Provider>
);
const {container, getByText} = render(component);
await waitFor(() => getByText('hello'));
expect(getUpdatablePluginsMock.mock.calls.length).toBe(1);
expect(container).toMatchSnapshot();
});

View File

@@ -0,0 +1,669 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`load PluginInstaller list 1`] = `
<div>
<div
class="css-1v0y38i-Container e1hsqii15"
height="500"
>
<div
class="css-1lxv8hi-Container-Horizontal-SandyToolbarContainer e1ecpah20"
>
<span
class="ant-input-group-wrapper ant-input-search"
>
<span
class="ant-input-wrapper ant-input-group"
>
<input
class="ant-input"
placeholder="Search Flipper plugins..."
type="text"
value=""
/>
<span
class="ant-input-group-addon"
>
<button
class="ant-btn ant-btn-icon-only ant-input-search-button"
type="button"
>
<span
aria-label="search"
class="anticon anticon-search"
role="img"
>
<svg
aria-hidden="true"
data-icon="search"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M909.6 854.5L649.9 594.8C690.2 542.7 712 479 712 412c0-80.2-31.3-155.4-87.9-212.1-56.6-56.7-132-87.9-212.1-87.9s-155.5 31.3-212.1 87.9C143.2 256.5 112 331.8 112 412c0 80.1 31.3 155.5 87.9 212.1C256.5 680.8 331.8 712 412 712c67 0 130.6-21.8 182.7-62l259.7 259.6a8.2 8.2 0 0011.6 0l43.6-43.5a8.2 8.2 0 000-11.6zM570.4 570.4C528 612.7 471.8 636 412 636s-116-23.3-158.4-65.6C211.3 528 188 471.8 188 412s23.3-116.1 65.6-158.4C296 211.3 352.2 188 412 188s116.1 23.2 158.4 65.6S636 352.2 636 412s-23.3 116.1-65.6 158.4z"
/>
</svg>
</span>
</button>
</span>
</span>
</span>
</div>
<div
class="css-bgfc37-View-FlexBox-FlexColumn-Container emab7y20"
tabindex="0"
>
<div
class="ant-dropdown-trigger css-18abd42-View-FlexBox-FlexColumn e1e47qlf0"
>
<div
class="css-1otvu18-View-FlexBox-FlexRow-TableHeadContainer eig1lcc1"
>
<div
class="css-mpoiz3-TableHeadColumnContainer eig1lcc0"
title="name"
width="0"
>
<div
class="eig1lcc3 css-x4q70f-InteractiveContainer-TableHeaderColumnInteractive e14xwmxq0"
style="z-index: auto; right: 0px; bottom: 0px; width: 100%; height: 100%;"
>
<div
class="css-1054ash-TableHeaderColumnContainer eig1lcc2"
>
Name
</div>
</div>
</div>
<div
class="css-mpoiz3-TableHeadColumnContainer eig1lcc0"
title="version"
width="0"
>
<div
class="eig1lcc3 css-x4q70f-InteractiveContainer-TableHeaderColumnInteractive e14xwmxq0"
style="z-index: auto; right: 0px; bottom: 0px; width: 100%; height: 100%;"
>
<div
class="css-1054ash-TableHeaderColumnContainer eig1lcc2"
>
Version
</div>
</div>
</div>
<div
class="css-mpoiz3-TableHeadColumnContainer eig1lcc0"
title="description"
width="0"
>
<div
class="eig1lcc3 css-x4q70f-InteractiveContainer-TableHeaderColumnInteractive e14xwmxq0"
style="z-index: auto; right: 0px; bottom: 0px; width: 100%; height: 100%;"
>
<div
class="css-1054ash-TableHeaderColumnContainer eig1lcc2"
>
Description
</div>
</div>
</div>
<div
class="css-mpoiz3-TableHeadColumnContainer eig1lcc0"
title="install"
width="0"
>
<div
class="eig1lcc3 css-x4q70f-InteractiveContainer-TableHeaderColumnInteractive e14xwmxq0"
style="z-index: auto; right: 0px; bottom: 0px; width: 100%; height: 100%;"
>
<div
class="css-1054ash-TableHeaderColumnContainer eig1lcc2"
>
</div>
</div>
</div>
</div>
</div>
<div
class="css-p5h61d-View-FlexBox-FlexColumn-Container emab7y20"
>
<div
class="ant-dropdown-trigger css-18abd42-View-FlexBox-FlexColumn e1e47qlf0"
>
<div
class="css-hg3ptm-View-FlexBox-FlexRow-TableBodyRowContainer e1pvjj0s1"
data-key="flipper-plugin-hello"
>
<div
class="css-yt4330-TableBodyColumnContainer e1pvjj0s0"
title=""
width="0"
>
<span
class="ant-typography ant-typography-ellipsis ant-typography-single-line ant-typography-ellipsis-single-line"
>
hello
</span>
</div>
<div
class="css-pfp0fy-TableBodyColumnContainer e1pvjj0s0"
title=""
width="0"
>
<span
class="ant-typography ant-typography-ellipsis ant-typography-single-line ant-typography-ellipsis-single-line"
>
0.1.0
</span>
</div>
<div
class="css-yt4330-TableBodyColumnContainer e1pvjj0s0"
title=""
width="0"
>
<div
class="css-s1wsbn-Container-Horizontal e1hsqii14"
>
<span
class="ant-typography ant-typography-ellipsis ant-typography-single-line ant-typography-ellipsis-single-line"
>
World?
</span>
<a
class="ant-typography"
href="https://yarnpkg.com/en/package/flipper-plugin-hello"
>
<div
class="css-1kmzf9v-ColoredIconCustom ekc8qeh0"
color="var(--light-color-button-active)"
size="16"
src="https://facebook.com/assets/?name=info-circle&variant=filled&size=16&set=facebook_icons&density=1x"
/>
</a>
</div>
</div>
<div
class="css-16v1lq1-TableBodyColumnContainer e1pvjj0s0"
title=""
width="0"
>
<button
class="ant-btn ant-btn-primary ant-btn-sm"
type="button"
>
<span>
Install
</span>
</button>
</div>
</div>
<div
class="css-hg3ptm-View-FlexBox-FlexRow-TableBodyRowContainer e1pvjj0s1"
data-key="flipper-plugin-world"
>
<div
class="css-yt4330-TableBodyColumnContainer e1pvjj0s0"
title=""
width="0"
>
<span
class="ant-typography ant-typography-ellipsis ant-typography-single-line ant-typography-ellipsis-single-line"
>
world
</span>
</div>
<div
class="css-pfp0fy-TableBodyColumnContainer e1pvjj0s0"
title=""
width="0"
>
<span
class="ant-typography ant-typography-ellipsis ant-typography-single-line ant-typography-ellipsis-single-line"
>
0.2.0
</span>
</div>
<div
class="css-yt4330-TableBodyColumnContainer e1pvjj0s0"
title=""
width="0"
>
<div
class="css-s1wsbn-Container-Horizontal e1hsqii14"
>
<span
class="ant-typography ant-typography-ellipsis ant-typography-single-line ant-typography-ellipsis-single-line"
>
Hello?
</span>
<a
class="ant-typography"
href="https://yarnpkg.com/en/package/flipper-plugin-world"
>
<div
class="css-1kmzf9v-ColoredIconCustom ekc8qeh0"
color="var(--light-color-button-active)"
size="16"
src="https://facebook.com/assets/?name=info-circle&variant=filled&size=16&set=facebook_icons&density=1x"
/>
</a>
</div>
</div>
<div
class="css-16v1lq1-TableBodyColumnContainer e1pvjj0s0"
title=""
width="0"
>
<button
class="ant-btn ant-btn-primary ant-btn-sm"
type="button"
>
<span>
Install
</span>
</button>
</div>
</div>
</div>
</div>
</div>
<div
class="css-1lxv8hi-Container-Horizontal-SandyToolbarContainer e1ecpah20"
>
<div
class="css-1spj5hr-View-FlexBox-FlexRow-Container ev83mp62"
>
<input
class="css-sli06x-Input-FileInputBox ev83mp60"
placeholder="Specify path to a Flipper package or just drag and drop it here..."
type="text"
value=""
/>
<div
class="css-ccdckn-View-FlexBox-FlexRow-GlyphContainer ev83mp61"
>
<img
alt="dots-3-circle"
class="ev83mp63 css-6iptsk-ColoredIconBlack-CenteredGlyph ekc8qeh1"
size="16"
src="https://facebook.com/assets/?name=dots-3-circle&variant=outline&size=16&set=facebook_icons&density=1x"
title="Open file selection dialog"
/>
</div>
<div
class="css-ccdckn-View-FlexBox-FlexRow-GlyphContainer ev83mp61"
>
<div
class="css-auhar3-TooltipContainer e1m67rki0"
>
<div
class="ev83mp63 css-1qsl9s4-ColoredIconCustom-CenteredGlyph ekc8qeh0"
color="#D79651"
size="16"
src="https://facebook.com/assets/?name=caution-triangle&variant=filled&size=16&set=facebook_icons&density=1x"
/>
</div>
</div>
</div>
<div
class="css-5ukfaz-View-FlexBox-FlexRow-ButtonContainer eguixfz1"
>
<div
class="css-wospjg-View-FlexBox-FlexRow ek54xq0"
>
<button
class="ant-btn ant-btn-primary"
disabled=""
type="button"
>
<span>
Install
</span>
</button>
<div
class="css-170i4ha-View-FlexBox-FlexRow-ErrorGlyphContainer eguixfz0"
/>
</div>
</div>
</div>
</div>
</div>
`;
exports[`load PluginInstaller list with one plugin installed 1`] = `
<div>
<div
class="css-1v0y38i-Container e1hsqii15"
height="500"
>
<div
class="css-1lxv8hi-Container-Horizontal-SandyToolbarContainer e1ecpah20"
>
<span
class="ant-input-group-wrapper ant-input-search"
>
<span
class="ant-input-wrapper ant-input-group"
>
<input
class="ant-input"
placeholder="Search Flipper plugins..."
type="text"
value=""
/>
<span
class="ant-input-group-addon"
>
<button
class="ant-btn ant-btn-icon-only ant-input-search-button"
type="button"
>
<span
aria-label="search"
class="anticon anticon-search"
role="img"
>
<svg
aria-hidden="true"
data-icon="search"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M909.6 854.5L649.9 594.8C690.2 542.7 712 479 712 412c0-80.2-31.3-155.4-87.9-212.1-56.6-56.7-132-87.9-212.1-87.9s-155.5 31.3-212.1 87.9C143.2 256.5 112 331.8 112 412c0 80.1 31.3 155.5 87.9 212.1C256.5 680.8 331.8 712 412 712c67 0 130.6-21.8 182.7-62l259.7 259.6a8.2 8.2 0 0011.6 0l43.6-43.5a8.2 8.2 0 000-11.6zM570.4 570.4C528 612.7 471.8 636 412 636s-116-23.3-158.4-65.6C211.3 528 188 471.8 188 412s23.3-116.1 65.6-158.4C296 211.3 352.2 188 412 188s116.1 23.2 158.4 65.6S636 352.2 636 412s-23.3 116.1-65.6 158.4z"
/>
</svg>
</span>
</button>
</span>
</span>
</span>
</div>
<div
class="css-bgfc37-View-FlexBox-FlexColumn-Container emab7y20"
tabindex="0"
>
<div
class="ant-dropdown-trigger css-18abd42-View-FlexBox-FlexColumn e1e47qlf0"
>
<div
class="css-1otvu18-View-FlexBox-FlexRow-TableHeadContainer eig1lcc1"
>
<div
class="css-mpoiz3-TableHeadColumnContainer eig1lcc0"
title="name"
width="0"
>
<div
class="eig1lcc3 css-x4q70f-InteractiveContainer-TableHeaderColumnInteractive e14xwmxq0"
style="z-index: auto; right: 0px; bottom: 0px; width: 100%; height: 100%;"
>
<div
class="css-1054ash-TableHeaderColumnContainer eig1lcc2"
>
Name
</div>
</div>
</div>
<div
class="css-mpoiz3-TableHeadColumnContainer eig1lcc0"
title="version"
width="0"
>
<div
class="eig1lcc3 css-x4q70f-InteractiveContainer-TableHeaderColumnInteractive e14xwmxq0"
style="z-index: auto; right: 0px; bottom: 0px; width: 100%; height: 100%;"
>
<div
class="css-1054ash-TableHeaderColumnContainer eig1lcc2"
>
Version
</div>
</div>
</div>
<div
class="css-mpoiz3-TableHeadColumnContainer eig1lcc0"
title="description"
width="0"
>
<div
class="eig1lcc3 css-x4q70f-InteractiveContainer-TableHeaderColumnInteractive e14xwmxq0"
style="z-index: auto; right: 0px; bottom: 0px; width: 100%; height: 100%;"
>
<div
class="css-1054ash-TableHeaderColumnContainer eig1lcc2"
>
Description
</div>
</div>
</div>
<div
class="css-mpoiz3-TableHeadColumnContainer eig1lcc0"
title="install"
width="0"
>
<div
class="eig1lcc3 css-x4q70f-InteractiveContainer-TableHeaderColumnInteractive e14xwmxq0"
style="z-index: auto; right: 0px; bottom: 0px; width: 100%; height: 100%;"
>
<div
class="css-1054ash-TableHeaderColumnContainer eig1lcc2"
>
</div>
</div>
</div>
</div>
</div>
<div
class="css-p5h61d-View-FlexBox-FlexColumn-Container emab7y20"
>
<div
class="ant-dropdown-trigger css-18abd42-View-FlexBox-FlexColumn e1e47qlf0"
>
<div
class="css-hg3ptm-View-FlexBox-FlexRow-TableBodyRowContainer e1pvjj0s1"
data-key="flipper-plugin-hello"
>
<div
class="css-yt4330-TableBodyColumnContainer e1pvjj0s0"
title=""
width="0"
>
<span
class="ant-typography ant-typography-ellipsis ant-typography-single-line ant-typography-ellipsis-single-line"
>
hello
</span>
</div>
<div
class="css-pfp0fy-TableBodyColumnContainer e1pvjj0s0"
title=""
width="0"
>
<span
class="ant-typography ant-typography-ellipsis ant-typography-single-line ant-typography-ellipsis-single-line"
>
0.1.0
</span>
</div>
<div
class="css-yt4330-TableBodyColumnContainer e1pvjj0s0"
title=""
width="0"
>
<div
class="css-s1wsbn-Container-Horizontal e1hsqii14"
>
<span
class="ant-typography ant-typography-ellipsis ant-typography-single-line ant-typography-ellipsis-single-line"
>
World?
</span>
<a
class="ant-typography"
href="https://yarnpkg.com/en/package/flipper-plugin-hello"
>
<div
class="css-1kmzf9v-ColoredIconCustom ekc8qeh0"
color="var(--light-color-button-active)"
size="16"
src="https://facebook.com/assets/?name=info-circle&variant=filled&size=16&set=facebook_icons&density=1x"
/>
</a>
</div>
</div>
<div
class="css-16v1lq1-TableBodyColumnContainer e1pvjj0s0"
title=""
width="0"
>
<button
class="ant-btn ant-btn-sm"
type="button"
>
<span>
Remove
</span>
</button>
</div>
</div>
<div
class="css-hg3ptm-View-FlexBox-FlexRow-TableBodyRowContainer e1pvjj0s1"
data-key="flipper-plugin-world"
>
<div
class="css-yt4330-TableBodyColumnContainer e1pvjj0s0"
title=""
width="0"
>
<span
class="ant-typography ant-typography-ellipsis ant-typography-single-line ant-typography-ellipsis-single-line"
>
world
</span>
</div>
<div
class="css-pfp0fy-TableBodyColumnContainer e1pvjj0s0"
title=""
width="0"
>
<span
class="ant-typography ant-typography-ellipsis ant-typography-single-line ant-typography-ellipsis-single-line"
>
0.2.0
</span>
</div>
<div
class="css-yt4330-TableBodyColumnContainer e1pvjj0s0"
title=""
width="0"
>
<div
class="css-s1wsbn-Container-Horizontal e1hsqii14"
>
<span
class="ant-typography ant-typography-ellipsis ant-typography-single-line ant-typography-ellipsis-single-line"
>
Hello?
</span>
<a
class="ant-typography"
href="https://yarnpkg.com/en/package/flipper-plugin-world"
>
<div
class="css-1kmzf9v-ColoredIconCustom ekc8qeh0"
color="var(--light-color-button-active)"
size="16"
src="https://facebook.com/assets/?name=info-circle&variant=filled&size=16&set=facebook_icons&density=1x"
/>
</a>
</div>
</div>
<div
class="css-16v1lq1-TableBodyColumnContainer e1pvjj0s0"
title=""
width="0"
>
<button
class="ant-btn ant-btn-primary ant-btn-sm"
type="button"
>
<span>
Install
</span>
</button>
</div>
</div>
</div>
</div>
</div>
<div
class="css-1lxv8hi-Container-Horizontal-SandyToolbarContainer e1ecpah20"
>
<div
class="css-1spj5hr-View-FlexBox-FlexRow-Container ev83mp62"
>
<input
class="css-sli06x-Input-FileInputBox ev83mp60"
placeholder="Specify path to a Flipper package or just drag and drop it here..."
type="text"
value=""
/>
<div
class="css-ccdckn-View-FlexBox-FlexRow-GlyphContainer ev83mp61"
>
<img
alt="dots-3-circle"
class="ev83mp63 css-6iptsk-ColoredIconBlack-CenteredGlyph ekc8qeh1"
size="16"
src="https://facebook.com/assets/?name=dots-3-circle&variant=outline&size=16&set=facebook_icons&density=1x"
title="Open file selection dialog"
/>
</div>
<div
class="css-ccdckn-View-FlexBox-FlexRow-GlyphContainer ev83mp61"
>
<div
class="css-auhar3-TooltipContainer e1m67rki0"
>
<div
class="ev83mp63 css-1qsl9s4-ColoredIconCustom-CenteredGlyph ekc8qeh0"
color="#D79651"
size="16"
src="https://facebook.com/assets/?name=caution-triangle&variant=filled&size=16&set=facebook_icons&density=1x"
/>
</div>
</div>
</div>
<div
class="css-5ukfaz-View-FlexBox-FlexRow-ButtonContainer eguixfz1"
>
<div
class="css-wospjg-View-FlexBox-FlexRow ek54xq0"
>
<button
class="ant-btn ant-btn-primary"
disabled=""
type="button"
>
<span>
Install
</span>
</button>
<div
class="css-170i4ha-View-FlexBox-FlexRow-ErrorGlyphContainer eguixfz0"
/>
</div>
</div>
</div>
</div>
</div>
`;