Skip to content

Commit

Permalink
Merge pull request natcap#1644 from emlys/add-remove-plugin-interface
Browse files Browse the repository at this point in the history
Redesign plugin interface and support removing plugins
  • Loading branch information
dcdenu4 authored Oct 16, 2024
2 parents f5b33dc + e177bd8 commit 0c7bd6f
Show file tree
Hide file tree
Showing 8 changed files with 161 additions and 46 deletions.
1 change: 1 addition & 0 deletions workbench/src/main/ipcMainChannels.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export const ipcMainChannels = {
LOGGER: 'logger',
OPEN_EXTERNAL_URL: 'open-external-url',
OPEN_LOCAL_HTML: 'open-local-html',
REMOVE_PLUGIN: 'remove-plugin',
SET_SETTING: 'set-setting',
SHOW_ITEM_IN_FOLDER: 'show-item-in-folder',
SHOW_OPEN_DIALOG: 'show-open-dialog',
Expand Down
3 changes: 2 additions & 1 deletion workbench/src/main/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import {
setupLaunchPluginServerHandler,
setupInvestLogReaderHandler
} from './setupInvestHandlers';
import setupAddPlugin from './setupAddPlugin';
import { setupAddPlugin, setupRemovePlugin } from './setupAddRemovePlugin';
import setupGetNCPUs from './setupGetNCPUs';
import setupOpenExternalUrl from './setupOpenExternalUrl';
import setupOpenLocalHtml from './setupOpenLocalHtml';
Expand Down Expand Up @@ -102,6 +102,7 @@ export const createWindow = async () => {
setupOpenExternalUrl();
setupRendererLogger();
setupAddPlugin();
setupRemovePlugin();

const devModeArg = ELECTRON_DEV_MODE ? '--devmode' : '';
// Create the browser window.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { settingsStore } from './settingsStore';

const logger = getLogger(__filename.split('/').slice(-1)[0]);

export default function setupAddPlugin() {
export function setupAddPlugin() {
ipcMain.handle(
ipcMainChannels.ADD_PLUGIN,
(e, pluginURL) => {
Expand Down Expand Up @@ -70,3 +70,27 @@ export default function setupAddPlugin() {
}
);
}

export function setupRemovePlugin() {
ipcMain.handle(
ipcMainChannels.REMOVE_PLUGIN,
(e, pluginID) => {
logger.info('removing plugin', pluginID);
try {
// Delete the plugin's conda env
const env = settingsStore.get(`plugins.${pluginID}.env`);
const mamba = settingsStore.get('mamba');
execSync(
`${mamba} remove --yes --prefix ${env} --all`,
{ stdio: 'inherit' }
);
// Delete the plugin's data from storage
settingsStore.delete(`plugins.${pluginID}`);
logger.info('successfully removed plugin');
} catch (error) {
logger.info('Error removing plugin:');
logger.info(error);
}
}
);
}
9 changes: 6 additions & 3 deletions workbench/src/renderer/app.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,14 @@ import InvestTab from './components/InvestTab';
import SettingsModal from './components/SettingsModal';
import DataDownloadModal from './components/DataDownloadModal';
import DownloadProgressBar from './components/DownloadProgressBar';
import PluginModal from './components/PluginModal';
import InvestJob from './InvestJob';
import { dragOverHandlerNone } from './utils';

const { ipcRenderer } = window.Workbench.electron;
import { ipcMainChannels } from '../main/ipcMainChannels';
import { getInvestModelNames } from './server_requests';

const { ipcRenderer } = window.Workbench.electron;

/** This component manages any application state that should persist
* and be independent from properties of a single invest job.
*/
Expand Down Expand Up @@ -347,6 +348,9 @@ export default class App extends React.Component {
)
: <div />
}
<PluginModal
updateInvestList={this.updateInvestList}
/>
<SettingsModal
className="mx-3"
clearJobsStorage={this.clearRecentJobs}
Expand All @@ -372,7 +376,6 @@ export default class App extends React.Component {
openInvestModel={this.openInvestModel}
recentJobs={recentJobs}
batchUpdateArgs={this.batchUpdateArgs}
updateInvestList={this.updateInvestList}
/>
)
: <div />}
Expand Down
7 changes: 1 addition & 6 deletions workbench/src/renderer/components/HomeTab/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import { useTranslation } from 'react-i18next';

import OpenButton from '../OpenButton';
import InvestJob from '../../InvestJob';
import PluginModal from '../PluginModal';

const { logger } = window.Workbench;

Expand All @@ -36,7 +35,7 @@ export default class HomeTab extends React.Component {
}

render() {
const { recentJobs, investList, openInvestModel, updateInvestList } = this.props;
const { recentJobs, investList, openInvestModel } = this.props;

let sortedModelIds = {};
if (investList) {
Expand Down Expand Up @@ -90,9 +89,6 @@ export default class HomeTab extends React.Component {
{investButtons}
</ListGroup>
</Col>
<PluginModal
updateInvestList={updateInvestList}
/>
<Col className="recent-job-card-col">
<RecentInvestJobs
openInvestModel={openInvestModel}
Expand All @@ -111,7 +107,6 @@ HomeTab.propTypes = {
type: PropTypes.string,
})
).isRequired,
updateInvestList: PropTypes.func.isRequired,
openInvestModel: PropTypes.func.isRequired,
recentJobs: PropTypes.arrayOf(
PropTypes.shape({
Expand Down
111 changes: 82 additions & 29 deletions workbench/src/renderer/components/PluginModal/index.jsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import React, { useState } from 'react';
import React, { useEffect, useState } from 'react';
import PropTypes from 'prop-types';

import Button from 'react-bootstrap/Button';
import Form from 'react-bootstrap/Form';
import Modal from 'react-bootstrap/Modal';
import Spinner from 'react-bootstrap/Spinner';
import { MdOutlineAdd } from 'react-icons/md';
import { useTranslation } from 'react-i18next';

import { ipcMainChannels } from '../../../main/ipcMainChannels';
Expand All @@ -14,51 +13,106 @@ const { ipcRenderer } = window.Workbench.electron;

export default function PluginModal(props) {
const { updateInvestList } = props;
const [showAddPluginModal, setShowAddPluginModal] = useState(false);
const [showPluginModal, setShowPluginModal] = useState(false);
const [url, setURL] = useState(undefined);
const [err, setErr] = useState(undefined);
const [pluginToRemove, setPluginToRemove] = useState(undefined);
const [loading, setLoading] = useState(false);
const [plugins, setPlugins] = useState({});

const handleModalClose = () => setShowAddPluginModal(false);
const handleModalOpen = () => setShowAddPluginModal(true);
const handleSubmit = () => {
const handleModalClose = () => {
setURL(undefined);
setErr(false);
setShowPluginModal(false);
};
const handleModalOpen = () => setShowPluginModal(true);
const addPlugin = () => {
setLoading(true);
ipcRenderer.invoke(ipcMainChannels.ADD_PLUGIN, url).then((addPluginErr) => {
setLoading(false);
updateInvestList();
if (addPluginErr) {
setErr(addPluginErr);
} else {
setShowAddPluginModal(false);
setShowPluginModal(false);
}
});
};
const handleChange = (event) => {
setURL(event.currentTarget.value);

const removePlugin = () => {
setLoading(true);
ipcRenderer.invoke(ipcMainChannels.REMOVE_PLUGIN, pluginToRemove).then(() => {
updateInvestList();
setLoading(false);
setShowPluginModal(false);
});
};

useEffect(() => {
ipcRenderer.invoke(ipcMainChannels.GET_SETTING, 'plugins').then(
(data) => {
if (data) {
setPlugins(data);
setPluginToRemove(Object.keys(data)[0]);
}
}
);
}, [loading]);

const { t } = useTranslation();

let modalBody = (
<Modal.Body>
<Form>
<Form.Group className="mb-3">
<Form.Label htmlFor="url">Git URL</Form.Label>
<Form.Label htmlFor="url">{t('Add a plugin')}</Form.Label>
<Form.Control
id="url"
name="url"
type="text"
placeholder={t('Enter Git URL')}
onChange={handleChange}
onChange={(event) => setURL(event.currentTarget.value)}
/>
<Form.Text className="text-muted">
{t('This may take several minutes')}
</Form.Text>
<Button
disabled={loading}
className="mt-2"
onClick={addPlugin}
>
{t('Add')}
</Button>
</Form.Group>
<hr />
<Form.Group className="mb-3">
<Form.Label htmlFor="plugin-select">{t('Remove a plugin')}</Form.Label>
<Form.Control
id="plugin-select"
as="select"
value={pluginToRemove}
onChange={(event) => setPluginToRemove(event.currentTarget.value)}
>
{
Object.keys(plugins).map(
(pluginID) => (
<option
value={pluginID}
key={pluginID}
>
{plugins[pluginID].model_name}
</option>
)
)
}
</Form.Control>
<Button
disabled={loading || !Object.keys(plugins).length}
className="mt-2"
onClick={removePlugin}
>
{t('Remove')}
</Button>
</Form.Group>

<Button
name="submit"
onClick={handleSubmit}
>
{t('Add')}
</Button>
</Form>
</Modal.Body>
);
Expand All @@ -75,20 +129,19 @@ export default function PluginModal(props) {

return (
<React.Fragment>
<Button onClick={handleModalOpen}>
<MdOutlineAdd className="mr-1" />
{t('Add a plugin')}
<Button onClick={handleModalOpen} variant="outline-dark">
{t('Manage plugins')}
</Button>

<Modal show={showAddPluginModal} onHide={handleModalClose}>
<Modal show={showPluginModal} onHide={handleModalClose}>
<Modal.Header>
<Modal.Title>{t('Add a plugin')}</Modal.Title>
<Modal.Title>{t('Manage plugins')}</Modal.Title>
{loading && (
<Spinner animation="border" role="status" className="m-2">
<span className="sr-only">Loading...</span>
</Spinner>
)}
</Modal.Header>
{loading && (
<Spinner animation="border" role="status">
<span className="sr-only">Loading...</span>
</Spinner>
)}
{modalBody}
</Modal>
</React.Fragment>
Expand Down
1 change: 1 addition & 0 deletions workbench/tests/main/main.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,7 @@ describe('createWindow', () => {
await createWindow();
const expectedHandleChannels = [
ipcMainChannels.ADD_PLUGIN,
ipcMainChannels.REMOVE_PLUGIN,
ipcMainChannels.CHANGE_LANGUAGE,
ipcMainChannels.CHECK_STORAGE_TOKEN,
ipcMainChannels.CHECK_FILE_PERMISSIONS,
Expand Down
49 changes: 43 additions & 6 deletions workbench/tests/renderer/plugin.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,6 @@ describe('Add plugin modal', () => {
});

test('Interface to add a plugin', async () => {
const {
findByText, findByLabelText, findByRole, queryByRole,
} = render(<App />);
const spy = ipcRenderer.invoke.mockImplementation((channel, setting) => {
if (channel === ipcMainChannels.GET_SETTING) {
if (setting === 'plugins') {
Expand All @@ -64,11 +61,14 @@ describe('Add plugin modal', () => {
}
return Promise.resolve();
});
const {
findByText, findByLabelText, findByRole, queryByRole,
} = render(<App />);

const addPluginButton = await findByText('Add a plugin');
userEvent.click(addPluginButton);
const managePluginsButton = await findByText('Manage plugins');
userEvent.click(managePluginsButton);

const urlField = await findByLabelText('Git URL');
const urlField = await findByLabelText('Add a plugin');
await userEvent.type(urlField, 'fake url', { delay: 0 });
const submitButton = await findByText('Add');
userEvent.click(submitButton);
Expand Down Expand Up @@ -123,4 +123,41 @@ describe('Add plugin modal', () => {
);
});
});

test('Remove a plugin', async () => {
let plugins = {
foo: {
model_name: 'Foo',
type: 'plugin',
},
};
const spy = ipcRenderer.invoke.mockImplementation((channel, setting) => {
if (channel === ipcMainChannels.GET_SETTING) {
if (setting === 'plugins') {
return Promise.resolve(plugins);
}
} else if (channel === ipcMainChannels.REMOVE_PLUGIN) {
plugins = {};
}
return Promise.resolve();
});
const {
findByText, getByRole, findByLabelText, queryByRole,
} = render(<App />);

const managePluginsButton = await findByText('Manage plugins');
userEvent.click(managePluginsButton);

const pluginDropdown = await findByLabelText('Remove a plugin');
await userEvent.selectOptions(pluginDropdown, [getByRole('option', { name: 'Foo' })]);

const submitButton = await findByText('Remove');
userEvent.click(submitButton);
await waitFor(() => {
expect(spy.mock.calls.map((call) => call[0])).toContain(ipcMainChannels.REMOVE_PLUGIN);
});
// expect the plugin to have disappeared from the model list and the dropdown
await waitFor(() => expect(queryByRole('button', { name: /Foo/ })).toBeNull());
await waitFor(() => expect(queryByRole('option', { name: /Foo/ })).toBeNull());
});
});

0 comments on commit 0c7bd6f

Please sign in to comment.