Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

3418 available plugins #3620

Closed
wants to merge 10 commits into from
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ def _get_plugin_information_from_request(
raise ValueError(message)

try:
plugin_version = PluginVersion(**plugin_version_arg)
plugin_version = PluginVersion.parse(plugin_version_arg)
except ValueError as err:
message = f"Invalid plugin version argument: {plugin_version_arg}: {err}."
raise ValueError(message)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import React, {useState} from 'react';
import React, {useEffect, useState} from 'react';
import Tabs from '@mui/material/Tabs';
import {Tab, Box} from '@mui/material';
import {PluginsContext} from '../ui-components/plugins-marketplace/PluginsContext';
import AvailablePlugins from '../ui-components/plugins-marketplace/AvailablePlugins';
import AuthComponent from '../AuthComponent';

const TabPanel = (props) => {
const {children, value, index, ...other} = props;
Expand Down Expand Up @@ -33,13 +35,30 @@ const MarketplacePage = () => {
const [availablePlugins, setAvailablePlugins] = useState([]);
const [installedPlugins, setInstalledPlugins] = useState([]);
const [tabValue, setTabValue] = useState(0);
const authComponent = new AuthComponent({});

useEffect(() => {
authComponent.authFetch('/api/agent-plugins/available/index', {}, true).then(res => res.json()).then(plugins => {
setAvailablePlugins(plugins.plugins);
});
}, []);

useEffect(() => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not do it in the first useEffect?

refreshInstalledPlugins();
}, []);

const refreshInstalledPlugins = () => {
authComponent.authFetch('/api/agent-plugins/installed/manifests', {}, true).then(res => res.json()).then(plugins => {
setInstalledPlugins(plugins);
});
};

const handleChange = (_event, newValue) => {
setTabValue(newValue);
};

return (
<PluginsContext.Provider value={{availablePlugins, installedPlugins, setAvailablePlugins, setInstalledPlugins}}>
<PluginsContext.Provider value={{availablePlugins, installedPlugins, setAvailablePlugins, setInstalledPlugins, refreshInstalledPlugins}}>
<Box className="main col-xl-8 col-lg-8 col-md-9 col-sm-9 offset-xl-2 offset-lg-3 offset-md-3 offset-sm-3">
<h1 className='page-title'>Plugins</h1>
<Box sx={{borderBottom: 1, borderColor: 'divider'}}>
Expand All @@ -53,7 +72,7 @@ const MarketplacePage = () => {
<Tab label="Installed Plugins" {...a11yProps(1)}/>
</Tabs>
</Box>
<TabPanel value={tabValue} index={0}>AvailablePlugins</TabPanel>
<TabPanel value={tabValue} index={0}><AvailablePlugins /></TabPanel>
<TabPanel value={tabValue} index={1}>Installed Plugins</TabPanel>
</Box>
</PluginsContext.Provider>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import React, {useEffect, useState} from 'react';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's a PR for that component.
Make sure not to override it before merging.

import {Box, IconButton, InputAdornment, TextField} from '@mui/material';
import SearchIcon from '@mui/icons-material/Search';
import ClearIcon from '@mui/icons-material/Clear';
import {nanoid} from 'nanoid';

const EMPTY_STRING = '';
const SearchBar = (props) => {
const {variant = 'standard', label, placeholder = 'Search', setQuery} = {...props};
const [currentValue, setCurrentValue] = useState(EMPTY_STRING);

useEffect(() => {
setQuery && setQuery(currentValue);
}, [currentValue]);

const handleValueChange = (e) => {
const currentValue = e?.target?.value?.trim() || EMPTY_STRING;
setCurrentValue(currentValue);
}

const clearValue = () => {
setCurrentValue(EMPTY_STRING);
}

return (
<Box>
<TextField id={`search-bar-${nanoid()}`}
variant={variant}
label={label}
value={currentValue}
placeholder={placeholder}
onChange={handleValueChange}
InputProps={{
startAdornment: (
<InputAdornment position="start">
<SearchIcon/>
</InputAdornment>
),
endAdornment: (
currentValue !== '' && (
<InputAdornment position="end">
<IconButton onClick={clearValue}>
<ClearIcon/>
</IconButton>
</InputAdornment>
)
)
}}/>
</Box>
)
}

export default SearchBar;
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import React, {useContext, useState} from 'react';
import {installPlugin} from './mocksHelper';
import {shallowAdditionOfUniqueValueToArray, shallowRemovalOfUniqueValueFromArray} from '../../../utils/objectUtils';
import {PluginsContext} from './PluginsContext';
import {GridActionsCellItem} from '@mui/x-data-grid';
import {nanoid} from 'nanoid';
import FileDownloadIcon from '@mui/icons-material/FileDownload';
import DownloadDoneIcon from '@mui/icons-material/DownloadDone';
import DownloadingIcon from '@mui/icons-material/Downloading';
import BasePlugins from './BasePlugins';
import SearchBar from '../SearchBar';
import {Box} from '@mui/material';
import AuthComponent from '../../AuthComponent';

// Provides the plugins, filtering out the installed plugins
class AvailablePluginsView {
availablePlugins = []; // all plugins, grouped by type then name
installedPlugins = []; // all installed plugins, grouped by type then name

constructor(availablePlugins, installedPlugins) {
this.availablePlugins = availablePlugins;
this.installedPlugins = installedPlugins;
}

pluginInstalled(plugin_type, plugin_name) {
return plugin_type in this.installedPlugins && plugin_name in this.installedPlugins[plugin_type];
}

* makeAvailablePluginsIterator() {
for (const plugin_type in this.availablePlugins) {
for (const plugin_name in this.availablePlugins[plugin_type]) {
if (!this.pluginInstalled(plugin_type, plugin_name)) {
// There may be multiple versions of the same plugin, so we only want the latest one
yield this.availablePlugins[plugin_type][plugin_name].slice(-1)[0];
}
}
}
}

[Symbol.iterator]() {
return this.makeAvailablePluginsIterator();
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is object-oriented approach in an otherwise functional code. We can transform the data if we need a list, but creating our own collection that's sometimes object sometimes list complicates usage

};

const AvailablePlugins = () => {
const {availablePlugins} = useContext(PluginsContext);
const {installedPlugins} = useContext(PluginsContext);
const {refreshInstalledPlugins} = useContext(PluginsContext);
const availablePluginsView = new AvailablePluginsView(availablePlugins, installedPlugins);

const [successfullyInstalledPluginsIds, setSuccessfullyInstalledPluginsIds] = useState([]);
const [pluginsInInstallationProcess, setPluginsInInstallationProcess] = useState([]);
const authComponent = new AuthComponent({});

const onRefreshCallback = () => {
setSuccessfullyInstalledPluginsIds([]);
}

const onInstallClick = (pluginId, pluginName, pluginType, pluginVersion) => {
console.log('installing plugin: ', pluginName)

setPluginsInInstallationProcess((prevState) => {
return shallowAdditionOfUniqueValueToArray(prevState, pluginId);
});

authComponent.authFetch('/api/install-agent-plugin', {method: 'PUT', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({plugin_type: pluginType, name: pluginName, version: pluginVersion})}, true).then(() => {
setSuccessfullyInstalledPluginsIds((prevState) => {
return shallowAdditionOfUniqueValueToArray(prevState, pluginId);
});
refreshInstalledPlugins();
}).catch(() => {
console.log('error installing plugin');
}).finally(() => {
setPluginsInInstallationProcess((prevState => {
return shallowRemovalOfUniqueValueFromArray(prevState, pluginId);
}));
});
};

const getRowActions = (row) => {
const pluginId = row.id;
if (pluginsInInstallationProcess.includes(pluginId)) {
return [
<GridActionsCellItem
key={nanoid()}
icon={<DownloadingIcon/>}
label="Downloading"
className="textPrimary"
color="inherit"
/>
]
}

if (successfullyInstalledPluginsIds.includes(pluginId)) {
return [
<GridActionsCellItem
key={nanoid()}
icon={<DownloadDoneIcon/>}
label="Download Done"
className="textPrimary"
color="inherit"
/>
]
}

const pluginName = row.name;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

move these declarations to the top of the function.

const pluginType = row.type;
const pluginVersion = row.version;
return [
<GridActionsCellItem
key={nanoid()}
icon={<FileDownloadIcon/>}
label="Download"
className="textPrimary"
onClick={() => onInstallClick(pluginId, pluginName, pluginType, pluginVersion)}
color="inherit"
/>
];
}

return (
<Box>
<SearchBar />
<BasePlugins plugins={availablePluginsView}
loadingMessage="Loading all available plugins..."
onRefreshCallback={onRefreshCallback}
getRowActions={getRowActions}
/>
</Box>
)
};

export default AvailablePlugins;
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import React, {useMemo, useState} from 'react';
import {Box} from '@mui/material';
import XDataGrid from '../XDataGrid';
import {getPluginsGridHeaders, getPluginsGridRows} from './mocksHelper';
import styles from '../../../styles/components/plugins-marketplace/BasePlugins.module.scss';

const DEFAULT_LOADING_MESSAGE = 'Loading plugins...';
const initialState = {
sorting: {
sortModel: [{field: 'name', sort: 'asc'}]
}
};

const BasePlugins = (props) => {
const {plugins, getRowActions, onRefreshCallback, loadingMessage = DEFAULT_LOADING_MESSAGE} = {...props};

const [isLoadingPlugins, setIsLoadingPlugins] = useState(false);

const rows = useMemo(() => {
return getPluginsGridRows(plugins);
}, [plugins]);

// // eslint-disable-next-line no-unused-vars
// const onRefreshClick = () => {
// setPluginsList();
// if(onRefreshCallback) {
// onRefreshCallback();
// }
// }

return (
<Box className={styles['plugins-wrapper']}>
{/*<PluginsActions showUpgradableToggle={showUpgradableToggle}/>*/}

{isLoadingPlugins
? loadingMessage
: <XDataGrid columns={getPluginsGridHeaders(getRowActions)}
rows={[...rows]}
rowHeight={'25px'}
showToolbar={false}
maxHeight={'500px'}
className="marketplace-plugins-list"
initialState={initialState}
needCustomWorkaround={false}
setFlex={false}/>
}
</Box>
)
}

export default BasePlugins;
Loading