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';
|
||||
showWelcomeAtStartup: boolean;
|
||||
suppressPluginErrors: boolean;
|
||||
/**
|
||||
* Plugin marketplace - allow internal plugin distribution
|
||||
*/
|
||||
enablePluginMarketplace: boolean;
|
||||
marketplaceURL: string;
|
||||
enablePluginMarketplaceAutoUpdate: boolean;
|
||||
};
|
||||
|
||||
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(
|
||||
`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',
|
||||
showWelcomeAtStartup: true,
|
||||
suppressPluginErrors: false,
|
||||
enablePluginMarketplace: false,
|
||||
marketplaceURL: '',
|
||||
enablePluginMarketplaceAutoUpdate: true,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -18,7 +18,11 @@ import {connect} from 'react-redux';
|
||||
import {State as Store} from '../reducers';
|
||||
import {flush} from '../utils/persistor';
|
||||
import ToggledSection from './settings/ToggledSection';
|
||||
import {FilePathConfigField, ConfigText} from './settings/configFields';
|
||||
import {
|
||||
FilePathConfigField,
|
||||
ConfigText,
|
||||
URLConfigField,
|
||||
} from './settings/configFields';
|
||||
import KeyboardShortcutInput from './settings/KeyboardShortcutInput';
|
||||
import {isEqual, isMatch, isEmpty} from 'lodash';
|
||||
import LauncherSettingsPanel from '../fb-stubs/LauncherSettingsPanel';
|
||||
@@ -30,7 +34,12 @@ import {
|
||||
sleep,
|
||||
} from 'flipper-common';
|
||||
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 {loadTheme} from '../utils/loadTheme';
|
||||
|
||||
@@ -118,6 +127,9 @@ class SettingsSheet extends Component<Props, State> {
|
||||
reactNative,
|
||||
darkMode,
|
||||
suppressPluginErrors,
|
||||
enablePluginMarketplace,
|
||||
enablePluginMarketplaceAutoUpdate,
|
||||
marketplaceURL,
|
||||
} = this.state.updatedSettings;
|
||||
|
||||
const settingsPristine =
|
||||
@@ -324,6 +336,51 @@ class SettingsSheet extends Component<Props, State> {
|
||||
}}
|
||||
/>
|
||||
</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>
|
||||
<span>Reset all new user tooltips</span>
|
||||
<ResetTooltips />
|
||||
|
||||
@@ -154,3 +154,60 @@ export function ConfigText(props: {content: string; frozen?: boolean}) {
|
||||
</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) {
|
||||
// plugins not yet fetched
|
||||
// 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) {
|
||||
|
||||
@@ -27,6 +27,7 @@ import {isConnectivityOrAuthError} from 'flipper-common';
|
||||
import {isLoggedIn} from '../fb-stubs/user';
|
||||
import {getRenderHostInstance} from '..';
|
||||
|
||||
// TODO: provide this value from settings
|
||||
export const pollingIntervalMs = getRenderHostInstance().serverConfig.env
|
||||
.FLIPPER_PLUGIN_AUTO_UPDATE_POLLING_INTERVAL
|
||||
? parseInt(
|
||||
@@ -36,25 +37,31 @@ export const pollingIntervalMs = getRenderHostInstance().serverConfig.env
|
||||
) // for manual testing we could set smaller interval
|
||||
: 300000; // 5 min by default
|
||||
|
||||
function isAutoUpdateDisabled() {
|
||||
function isAutoUpdateDisabled(store: Store) {
|
||||
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().serverConfig.env.FLIPPER_NO_PLUGIN_AUTO_UPDATE !==
|
||||
undefined
|
||||
);
|
||||
}
|
||||
|
||||
function isPluginMarketplaceDisabled() {
|
||||
function isPluginMarketplaceDisabled(store: Store) {
|
||||
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().serverConfig.env.FLIPPER_NO_PLUGIN_MARKETPLACE
|
||||
);
|
||||
}
|
||||
|
||||
export default (store: Store) => {
|
||||
if (isPluginMarketplaceDisabled()) {
|
||||
if (isPluginMarketplaceDisabled(store)) {
|
||||
console.warn(
|
||||
'Loading plugins from Plugin Marketplace disabled by GK or env var',
|
||||
);
|
||||
@@ -98,10 +105,10 @@ export default (store: Store) => {
|
||||
};
|
||||
};
|
||||
|
||||
export async function loadPluginsFromMarketplace(): Promise<
|
||||
MarketplacePluginDetails[]
|
||||
> {
|
||||
const availablePlugins = await loadAvailablePlugins();
|
||||
export async function loadPluginsFromMarketplace(
|
||||
store: Store,
|
||||
): Promise<MarketplacePluginDetails[]> {
|
||||
const availablePlugins = await loadAvailablePlugins(store);
|
||||
return selectCompatibleMarketplaceVersions(availablePlugins);
|
||||
}
|
||||
|
||||
@@ -111,7 +118,7 @@ async function refreshMarketplacePlugins(store: Store): Promise<void> {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const plugins = await loadPluginsFromMarketplace();
|
||||
const plugins = await loadPluginsFromMarketplace(store);
|
||||
store.dispatch(registerMarketplacePlugins(plugins));
|
||||
autoUpdatePlugins(store, plugins);
|
||||
} catch (err) {
|
||||
@@ -177,7 +184,7 @@ export function autoUpdatePlugins(
|
||||
}
|
||||
}
|
||||
}
|
||||
if (isAutoUpdateDisabled() || !isLoggedIn().get()) {
|
||||
if (isAutoUpdateDisabled(store)) {
|
||||
return;
|
||||
}
|
||||
for (const plugin of marketplacePlugins) {
|
||||
|
||||
@@ -8,9 +8,22 @@
|
||||
*/
|
||||
|
||||
import {MarketplacePluginDetails} from '../reducers/plugins';
|
||||
import {Store} from '../reducers/index';
|
||||
|
||||
export async function loadAvailablePlugins(): Promise<
|
||||
MarketplacePluginDetails[]
|
||||
> {
|
||||
export async function loadAvailablePlugins(
|
||||
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 [];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -150,6 +150,9 @@ function createStubRenderHost(): RenderHost {
|
||||
},
|
||||
showWelcomeAtStartup: false,
|
||||
suppressPluginErrors: false,
|
||||
enablePluginMarketplace: false,
|
||||
marketplaceURL: '',
|
||||
enablePluginMarketplaceAutoUpdate: true,
|
||||
},
|
||||
validWebSocketOrigins: [],
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user