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:
committed by
Facebook GitHub Bot
parent
54b7ce9308
commit
7e50c0466a
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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();
|
||||
});
|
||||
@@ -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>
|
||||
`;
|
||||
Reference in New Issue
Block a user