Enable Marketplace for Flipper (#3491)
Summary: This is PR on top of: https://github.com/facebook/flipper/pull/3473 It adds an option to Settings to allow distribution of marketplace plugins. Also includes a simple fetch function to retrieve data from external API/server. ## Changelog Allow marketplace plugins Pull Request resolved: https://github.com/facebook/flipper/pull/3491 Test Plan: 1. Enable marketplace 2. Provide custom marketplace server (it will serve the list of internal plugins with downloadURL) 3. Test if can see Available plugins and can download/remove the plugin 4. If new update for the plugin, it should also allow auto update Reviewed By: antonk52 Differential Revision: D34586339 Pulled By: nikoant fbshipit-source-id: c887982aa0f0f9abd3b5360f22e8692a2445d345
This commit is contained in:
committed by
Facebook GitHub Bot
parent
5b6000b424
commit
37ff34390a
@@ -39,6 +39,12 @@ export type Settings = {
|
|||||||
darkMode: 'dark' | 'light' | 'system';
|
darkMode: 'dark' | 'light' | 'system';
|
||||||
showWelcomeAtStartup: boolean;
|
showWelcomeAtStartup: boolean;
|
||||||
suppressPluginErrors: boolean;
|
suppressPluginErrors: boolean;
|
||||||
|
/**
|
||||||
|
* Plugin marketplace - allow internal plugin distribution
|
||||||
|
*/
|
||||||
|
enablePluginMarketplace: boolean;
|
||||||
|
marketplaceURL: string;
|
||||||
|
enablePluginMarketplaceAutoUpdate: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export enum ReleaseChannel {
|
export enum ReleaseChannel {
|
||||||
|
|||||||
@@ -138,7 +138,17 @@ export class PluginManager {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
if (response.headers['content-type'] !== 'application/octet-stream') {
|
function parseHeaderValue(header: string) {
|
||||||
|
const values = header.split(';');
|
||||||
|
// remove white space
|
||||||
|
return values.map((value) => value.trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
!parseHeaderValue(response.headers['content-type']).includes(
|
||||||
|
'application/octet-stream',
|
||||||
|
)
|
||||||
|
) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`It looks like you are not on VPN/Lighthouse. Unexpected content type received: ${response.headers['content-type']}.`,
|
`It looks like you are not on VPN/Lighthouse. Unexpected content type received: ${response.headers['content-type']}.`,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -73,6 +73,9 @@ function getDefaultSettings(): Settings {
|
|||||||
darkMode: 'light',
|
darkMode: 'light',
|
||||||
showWelcomeAtStartup: true,
|
showWelcomeAtStartup: true,
|
||||||
suppressPluginErrors: false,
|
suppressPluginErrors: false,
|
||||||
|
enablePluginMarketplace: false,
|
||||||
|
marketplaceURL: '',
|
||||||
|
enablePluginMarketplaceAutoUpdate: true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,11 @@ import {connect} from 'react-redux';
|
|||||||
import {State as Store} from '../reducers';
|
import {State as Store} from '../reducers';
|
||||||
import {flush} from '../utils/persistor';
|
import {flush} from '../utils/persistor';
|
||||||
import ToggledSection from './settings/ToggledSection';
|
import ToggledSection from './settings/ToggledSection';
|
||||||
import {FilePathConfigField, ConfigText} from './settings/configFields';
|
import {
|
||||||
|
FilePathConfigField,
|
||||||
|
ConfigText,
|
||||||
|
URLConfigField,
|
||||||
|
} from './settings/configFields';
|
||||||
import KeyboardShortcutInput from './settings/KeyboardShortcutInput';
|
import KeyboardShortcutInput from './settings/KeyboardShortcutInput';
|
||||||
import {isEqual, isMatch, isEmpty} from 'lodash';
|
import {isEqual, isMatch, isEmpty} from 'lodash';
|
||||||
import LauncherSettingsPanel from '../fb-stubs/LauncherSettingsPanel';
|
import LauncherSettingsPanel from '../fb-stubs/LauncherSettingsPanel';
|
||||||
@@ -30,7 +34,12 @@ import {
|
|||||||
sleep,
|
sleep,
|
||||||
} from 'flipper-common';
|
} from 'flipper-common';
|
||||||
import {Modal, message, Button} from 'antd';
|
import {Modal, message, Button} from 'antd';
|
||||||
import {Layout, withTrackingScope, _NuxManagerContext} from 'flipper-plugin';
|
import {
|
||||||
|
Layout,
|
||||||
|
withTrackingScope,
|
||||||
|
_NuxManagerContext,
|
||||||
|
NUX,
|
||||||
|
} from 'flipper-plugin';
|
||||||
import {getRenderHostInstance} from '../RenderHost';
|
import {getRenderHostInstance} from '../RenderHost';
|
||||||
import {loadTheme} from '../utils/loadTheme';
|
import {loadTheme} from '../utils/loadTheme';
|
||||||
|
|
||||||
@@ -118,6 +127,9 @@ class SettingsSheet extends Component<Props, State> {
|
|||||||
reactNative,
|
reactNative,
|
||||||
darkMode,
|
darkMode,
|
||||||
suppressPluginErrors,
|
suppressPluginErrors,
|
||||||
|
enablePluginMarketplace,
|
||||||
|
enablePluginMarketplaceAutoUpdate,
|
||||||
|
marketplaceURL,
|
||||||
} = this.state.updatedSettings;
|
} = this.state.updatedSettings;
|
||||||
|
|
||||||
const settingsPristine =
|
const settingsPristine =
|
||||||
@@ -324,6 +336,51 @@ class SettingsSheet extends Component<Props, State> {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</ToggledSection>
|
</ToggledSection>
|
||||||
|
<NUX
|
||||||
|
// TODO: provide link to Flipper doc with more details
|
||||||
|
title="Plugin marketplace serve as a way to distribute private/internal plugins"
|
||||||
|
placement="right">
|
||||||
|
<ToggledSection
|
||||||
|
label="Enable plugin marketplace"
|
||||||
|
toggled={enablePluginMarketplace}
|
||||||
|
frozen={false}
|
||||||
|
onChange={(v) => {
|
||||||
|
this.setState({
|
||||||
|
updatedSettings: {
|
||||||
|
...this.state.updatedSettings,
|
||||||
|
enablePluginMarketplace: v,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}}>
|
||||||
|
<URLConfigField
|
||||||
|
label="Martkeplace URL"
|
||||||
|
defaultValue={
|
||||||
|
marketplaceURL || 'http://plugin-marketplace.local/get-plugins'
|
||||||
|
}
|
||||||
|
onChange={(v) => {
|
||||||
|
this.setState({
|
||||||
|
updatedSettings: {
|
||||||
|
...this.state.updatedSettings,
|
||||||
|
marketplaceURL: v,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<ToggledSection
|
||||||
|
label="Enable auto update"
|
||||||
|
toggled={enablePluginMarketplaceAutoUpdate}
|
||||||
|
frozen={false}
|
||||||
|
onChange={(v) => {
|
||||||
|
this.setState({
|
||||||
|
updatedSettings: {
|
||||||
|
...this.state.updatedSettings,
|
||||||
|
enablePluginMarketplaceAutoUpdate: v,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</ToggledSection>
|
||||||
|
</NUX>
|
||||||
<Layout.Right center>
|
<Layout.Right center>
|
||||||
<span>Reset all new user tooltips</span>
|
<span>Reset all new user tooltips</span>
|
||||||
<ResetTooltips />
|
<ResetTooltips />
|
||||||
|
|||||||
@@ -154,3 +154,60 @@ export function ConfigText(props: {content: string; frozen?: boolean}) {
|
|||||||
</ConfigFieldContainer>
|
</ConfigFieldContainer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function URLConfigField(props: {
|
||||||
|
label: string;
|
||||||
|
resetValue?: string;
|
||||||
|
defaultValue: string;
|
||||||
|
onChange: (path: string) => void;
|
||||||
|
frozen?: boolean;
|
||||||
|
}) {
|
||||||
|
const [value, setValue] = useState(props.defaultValue);
|
||||||
|
const [isValid, setIsValid] = useState(true);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
try {
|
||||||
|
const url = new URL(value);
|
||||||
|
const isValidUrl =
|
||||||
|
['http:', 'https:'].includes(url.protocol) &&
|
||||||
|
url.href.startsWith(value);
|
||||||
|
setIsValid(isValidUrl);
|
||||||
|
} catch (_) {
|
||||||
|
setIsValid(false);
|
||||||
|
}
|
||||||
|
}, [value]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ConfigFieldContainer>
|
||||||
|
<InfoText>{props.label}</InfoText>
|
||||||
|
<FileInputBox
|
||||||
|
placeholder={props.label}
|
||||||
|
value={value}
|
||||||
|
isValid={isValid}
|
||||||
|
onChange={(e) => {
|
||||||
|
setValue(e.target.value);
|
||||||
|
props.onChange(e.target.value);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{props.resetValue && (
|
||||||
|
<FlexColumn
|
||||||
|
title={`Reset to default value ${props.resetValue}`}
|
||||||
|
onClick={() => {
|
||||||
|
setValue(props.resetValue!);
|
||||||
|
props.onChange(props.resetValue!);
|
||||||
|
}}>
|
||||||
|
<CenteredGlyph
|
||||||
|
color={theme.primaryColor}
|
||||||
|
name="undo"
|
||||||
|
variant="outline"
|
||||||
|
/>
|
||||||
|
</FlexColumn>
|
||||||
|
)}
|
||||||
|
{isValid ? null : (
|
||||||
|
<CenteredGlyph name="caution-triangle" color={colors.yellow} />
|
||||||
|
)}
|
||||||
|
{props.frozen && <GrayedOutOverlay />}
|
||||||
|
</ConfigFieldContainer>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -320,7 +320,7 @@ async function verifyPluginStatus(
|
|||||||
if (!isTest() && !store.getState().plugins.marketplacePlugins.length) {
|
if (!isTest() && !store.getState().plugins.marketplacePlugins.length) {
|
||||||
// plugins not yet fetched
|
// plugins not yet fetched
|
||||||
// updates plugins from marketplace (if logged in), and stores them
|
// updates plugins from marketplace (if logged in), and stores them
|
||||||
await loadPluginsFromMarketplace();
|
await loadPluginsFromMarketplace(store);
|
||||||
}
|
}
|
||||||
// while true loop; after pressing install or add GK, we want to check again if plugin is available
|
// while true loop; after pressing install or add GK, we want to check again if plugin is available
|
||||||
while (true) {
|
while (true) {
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import {isConnectivityOrAuthError} from 'flipper-common';
|
|||||||
import {isLoggedIn} from '../fb-stubs/user';
|
import {isLoggedIn} from '../fb-stubs/user';
|
||||||
import {getRenderHostInstance} from '..';
|
import {getRenderHostInstance} from '..';
|
||||||
|
|
||||||
|
// TODO: provide this value from settings
|
||||||
export const pollingIntervalMs = getRenderHostInstance().serverConfig.env
|
export const pollingIntervalMs = getRenderHostInstance().serverConfig.env
|
||||||
.FLIPPER_PLUGIN_AUTO_UPDATE_POLLING_INTERVAL
|
.FLIPPER_PLUGIN_AUTO_UPDATE_POLLING_INTERVAL
|
||||||
? parseInt(
|
? parseInt(
|
||||||
@@ -36,25 +37,31 @@ export const pollingIntervalMs = getRenderHostInstance().serverConfig.env
|
|||||||
) // for manual testing we could set smaller interval
|
) // for manual testing we could set smaller interval
|
||||||
: 300000; // 5 min by default
|
: 300000; // 5 min by default
|
||||||
|
|
||||||
function isAutoUpdateDisabled() {
|
function isAutoUpdateDisabled(store: Store) {
|
||||||
return (
|
return (
|
||||||
!getFlipperLib().isFB ||
|
// for open-source version auto-updates must be explicitly enabled in Settings
|
||||||
|
(!getFlipperLib().isFB &&
|
||||||
|
!store.getState().settingsState.enablePluginMarketplaceAutoUpdate) ||
|
||||||
|
// for internal build we disable auto-updates in case user is not logged
|
||||||
|
(getFlipperLib().isFB && !isLoggedIn().get()) ||
|
||||||
getRenderHostInstance().GK('flipper_disable_plugin_auto_update') ||
|
getRenderHostInstance().GK('flipper_disable_plugin_auto_update') ||
|
||||||
getRenderHostInstance().serverConfig.env.FLIPPER_NO_PLUGIN_AUTO_UPDATE !==
|
getRenderHostInstance().serverConfig.env.FLIPPER_NO_PLUGIN_AUTO_UPDATE !==
|
||||||
undefined
|
undefined
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function isPluginMarketplaceDisabled() {
|
function isPluginMarketplaceDisabled(store: Store) {
|
||||||
return (
|
return (
|
||||||
!getFlipperLib().isFB ||
|
// for open-source version marketplace must be explicitly enabled in Settings
|
||||||
|
(!getFlipperLib().isFB &&
|
||||||
|
!store.getState().settingsState.enablePluginMarketplace) ||
|
||||||
getRenderHostInstance().GK('flipper_disable_plugin_marketplace') ||
|
getRenderHostInstance().GK('flipper_disable_plugin_marketplace') ||
|
||||||
getRenderHostInstance().serverConfig.env.FLIPPER_NO_PLUGIN_MARKETPLACE
|
getRenderHostInstance().serverConfig.env.FLIPPER_NO_PLUGIN_MARKETPLACE
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default (store: Store) => {
|
export default (store: Store) => {
|
||||||
if (isPluginMarketplaceDisabled()) {
|
if (isPluginMarketplaceDisabled(store)) {
|
||||||
console.warn(
|
console.warn(
|
||||||
'Loading plugins from Plugin Marketplace disabled by GK or env var',
|
'Loading plugins from Plugin Marketplace disabled by GK or env var',
|
||||||
);
|
);
|
||||||
@@ -98,10 +105,10 @@ export default (store: Store) => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function loadPluginsFromMarketplace(): Promise<
|
export async function loadPluginsFromMarketplace(
|
||||||
MarketplacePluginDetails[]
|
store: Store,
|
||||||
> {
|
): Promise<MarketplacePluginDetails[]> {
|
||||||
const availablePlugins = await loadAvailablePlugins();
|
const availablePlugins = await loadAvailablePlugins(store);
|
||||||
return selectCompatibleMarketplaceVersions(availablePlugins);
|
return selectCompatibleMarketplaceVersions(availablePlugins);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -111,7 +118,7 @@ async function refreshMarketplacePlugins(store: Store): Promise<void> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const plugins = await loadPluginsFromMarketplace();
|
const plugins = await loadPluginsFromMarketplace(store);
|
||||||
store.dispatch(registerMarketplacePlugins(plugins));
|
store.dispatch(registerMarketplacePlugins(plugins));
|
||||||
autoUpdatePlugins(store, plugins);
|
autoUpdatePlugins(store, plugins);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -177,7 +184,7 @@ export function autoUpdatePlugins(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (isAutoUpdateDisabled() || !isLoggedIn().get()) {
|
if (isAutoUpdateDisabled(store)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
for (const plugin of marketplacePlugins) {
|
for (const plugin of marketplacePlugins) {
|
||||||
|
|||||||
@@ -8,9 +8,22 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import {MarketplacePluginDetails} from '../reducers/plugins';
|
import {MarketplacePluginDetails} from '../reducers/plugins';
|
||||||
|
import {Store} from '../reducers/index';
|
||||||
|
|
||||||
export async function loadAvailablePlugins(): Promise<
|
export async function loadAvailablePlugins(
|
||||||
MarketplacePluginDetails[]
|
store: Store,
|
||||||
> {
|
): Promise<MarketplacePluginDetails[]> {
|
||||||
|
const {enablePluginMarketplace, marketplaceURL} =
|
||||||
|
store.getState().settingsState;
|
||||||
|
try {
|
||||||
|
if (!enablePluginMarketplace && !marketplaceURL) {
|
||||||
|
throw new Error('Marketplace is not enabled');
|
||||||
|
}
|
||||||
|
const response = await fetch(marketplaceURL);
|
||||||
|
const plugins = await response.json();
|
||||||
|
return plugins;
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed while retrieving marketplace plugins', e);
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -150,6 +150,9 @@ function createStubRenderHost(): RenderHost {
|
|||||||
},
|
},
|
||||||
showWelcomeAtStartup: false,
|
showWelcomeAtStartup: false,
|
||||||
suppressPluginErrors: false,
|
suppressPluginErrors: false,
|
||||||
|
enablePluginMarketplace: false,
|
||||||
|
marketplaceURL: '',
|
||||||
|
enablePluginMarketplaceAutoUpdate: true,
|
||||||
},
|
},
|
||||||
validWebSocketOrigins: [],
|
validWebSocketOrigins: [],
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user