From 144338e74a753dfefce6fb8265a943329cbbe0a8 Mon Sep 17 00:00:00 2001 From: Pascal Hartig Date: Thu, 14 Nov 2019 08:51:33 -0800 Subject: [PATCH] Add update indicators to PluginInstaller Summary: Display is functional, the update itself isn't just yet. Want to keep this easier to review. Instead of GK, I just have a top-level toggle for now, because that will go away with one of the next diffs anyway. Reviewed By: jknoxville Differential Revision: D18479290 fbshipit-source-id: b49394d4ab681c9d1dc5db0e4bee54f9255494b9 --- src/chrome/PluginInstaller.tsx | 90 ++++++++++++++++--- src/chrome/__tests__/PluginInstaller.node.tsx | 57 ++++++++++++ 2 files changed, 135 insertions(+), 12 deletions(-) create mode 100644 src/chrome/__tests__/PluginInstaller.node.tsx diff --git a/src/chrome/PluginInstaller.tsx b/src/chrome/PluginInstaller.tsx index e995a1db8..b4f5f5f13 100644 --- a/src/chrome/PluginInstaller.tsx +++ b/src/chrome/PluginInstaller.tsx @@ -42,12 +42,15 @@ import { readInstalledPlugins, providePluginManager, provideSearchIndex, + findPluginUpdates, + UpdateResult, } from '../utils/pluginManager'; import {State as AppState} from '../reducers'; import {connect} from 'react-redux'; import {Dispatch, Action} from 'redux'; const TAG = 'PluginInstaller'; +const ENABLE_PLUGIN_UPDATES = false; const EllipsisText = styled(Text)({ overflow: 'hidden', @@ -114,6 +117,26 @@ const defaultProps: OwnProps = { autoHeight: false, }; +type UpdatablePlugin = { + updateStatus: UpdateResult; +}; + +type UpdatablePluginDefinition = PluginDefinition & UpdatablePlugin; + +// exported for testing +export function annotatePluginsWithUpdates( + installedPlugins: Map, + updates: Map, +): Map { + const annotated: Array<[string, UpdatablePluginDefinition]> = Array.from( + installedPlugins.entries(), + ).map(([key, value]) => { + const updateStatus = updates.get(key) || {kind: 'up-to-date'}; + return [key, {...value, updateStatus: updateStatus}]; + }); + return new Map(annotated); +} + const PluginInstaller = function props(props: Props) { const [restartRequired, setRestartRequired] = useState(false); const [query, setQuery] = useState(''); @@ -174,16 +197,25 @@ const AlignedGlyph = styled(Glyph)({ marginTop: 6, }); +function liftUpdatable(val: PluginDefinition): UpdatablePluginDefinition { + return { + ...val, + updateStatus: {kind: 'up-to-date'}, + }; +} + function InstallButton(props: { name: string; version: string; onInstall: () => void; installed: boolean; + updateStatus: UpdateResult; }) { type InstallAction = | {kind: 'Install'; error?: string} | {kind: 'Waiting'} - | {kind: 'Remove'; error?: string}; + | {kind: 'Remove'; error?: string} + | {kind: 'Update'; error?: string}; const catchError = ( actionKind: 'Install' | 'Remove', @@ -245,7 +277,11 @@ function InstallButton(props: { ); const [action, setAction] = useState( - props.installed ? {kind: 'Remove'} : {kind: 'Install'}, + props.updateStatus.kind === 'update-available' + ? {kind: 'Update'} + : props.installed + ? {kind: 'Remove'} + : {kind: 'Install'}, ); if (action.kind === 'Waiting') { @@ -256,12 +292,20 @@ function InstallButton(props: { const button = ( reportPlatformFailures(performInstall(), `${TAG}:install`) - : () => reportPlatformFailures(performRemove(), `${TAG}:remove`) - }> + 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': + alert('Not implemented yet.'); + break; + } + }}> {action.kind} ); @@ -305,7 +349,7 @@ function useNPMSearch( }, []); const createRow = useCallback( - (h: PluginDefinition) => ({ + (h: UpdatablePluginDefinition) => ({ key: h.name, columns: { name: {value: {h.name}}, @@ -331,6 +375,7 @@ function useNPMSearch( version={h.version} onInstall={onInstall} installed={installedPlugins.has(h.name)} + updateStatus={h.updateStatus} /> ), align: 'center' as 'center', @@ -340,7 +385,13 @@ function useNPMSearch( [installedPlugins], ); - const [searchResults, setSearchResults] = useState([]); + const [searchResults, setSearchResults] = useState< + UpdatablePluginDefinition[] + >([]); + const [ + updateAnnotatedInstalledPlugins, + setUpdateAnnotatedInstalledPlugins, + ] = useState>(new Map()); useEffect(() => { (async () => { @@ -353,12 +404,27 @@ function useNPMSearch( `${TAG}:queryIndex`, ); - setSearchResults(hits.filter(hit => !installedPlugins.has(hit.name))); + setSearchResults( + hits.filter(hit => !installedPlugins.has(hit.name)).map(liftUpdatable), + ); setQuery(query); })(); }, [query, installedPlugins]); - const results = Array.from(installedPlugins.values()).concat(searchResults); + useEffect(() => { + (async () => { + const updates = ENABLE_PLUGIN_UPDATES + ? new Map(await findPluginUpdates(installedPlugins)) + : new Map(); + setUpdateAnnotatedInstalledPlugins( + annotatePluginsWithUpdates(installedPlugins, updates), + ); + })(); + }, [installedPlugins]); + + const results = Array.from(updateAnnotatedInstalledPlugins.values()).concat( + searchResults, + ); return List(results.map(createRow)); } diff --git a/src/chrome/__tests__/PluginInstaller.node.tsx b/src/chrome/__tests__/PluginInstaller.node.tsx new file mode 100644 index 000000000..ee9f0751c --- /dev/null +++ b/src/chrome/__tests__/PluginInstaller.node.tsx @@ -0,0 +1,57 @@ +/** + * 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 {annotatePluginsWithUpdates} from '../PluginInstaller'; +import {UpdateResult} from '../../utils/pluginManager'; + +test('annotatePluginsWithUpdates', async () => { + const installedPlugins = new Map([ + [ + 'example', + { + name: 'example', + version: '0.1.0', + description: 'Gaze into the death crystal', + }, + ], + [ + 'ricksybusiness', + { + name: 'ricksybusiness', + version: '1.0.0', + description: 'Rick Die Rickpeat', + }, + ], + ]); + const updates = new Map([ + ['example', {kind: 'update-available', version: '1.1.0'}], + ]); + const res = annotatePluginsWithUpdates(installedPlugins, updates); + expect(res).toMatchInlineSnapshot(` + Map { + "example" => Object { + "description": "Gaze into the death crystal", + "name": "example", + "updateStatus": Object { + "kind": "update-available", + "version": "1.1.0", + }, + "version": "0.1.0", + }, + "ricksybusiness" => Object { + "description": "Rick Die Rickpeat", + "name": "ricksybusiness", + "updateStatus": Object { + "kind": "up-to-date", + }, + "version": "1.0.0", + }, + } + `); +});