diff --git a/monkey/monkey_island/cc/next_ui/src/app/(protected)/plugins/installed/InstalledPluginFilters.tsx b/monkey/monkey_island/cc/next_ui/src/app/(protected)/plugins/installed/InstalledPluginFilters.tsx
index 6b4592d6f79..a1ce0ff3ee1 100644
--- a/monkey/monkey_island/cc/next_ui/src/app/(protected)/plugins/installed/InstalledPluginFilters.tsx
+++ b/monkey/monkey_island/cc/next_ui/src/app/(protected)/plugins/installed/InstalledPluginFilters.tsx
@@ -9,15 +9,13 @@ import {
} from '@/app/(protected)/plugins/_lib/PluginTable';
import _ from 'lodash';
+type PluginFilterFunc = (row: PluginRow) => boolean;
+
type InstalledPluginFilterProps = {
setDisplayedRowsCallback: (rows: PluginRow[]) => void;
setIsFilteringCallback: (isFiltering: boolean) => void;
};
-export type FilterProps = {
- setFiltersCallback: (filters: any) => void;
-};
-
export const defaultSearchableColumns = [
'name',
'pluginType',
@@ -31,6 +29,15 @@ const InstalledPluginFilters = (props: InstalledPluginFilterProps) => {
const { data: installedPlugins } = useGetInstalledPluginsQuery();
const [filters, setFilters] = useState({});
+ const setFilterCallback = (
+ filterName: string,
+ filterFunc: PluginFilterFunc
+ ) => {
+ setFilters((prevState) => {
+ return { ...prevState, [filterName]: filterFunc };
+ });
+ };
+
const filterRows = (rows: PluginRow[]): PluginRow[] => {
setIsFilteringCallback(true);
let filteredRows = _.cloneDeep(rows);
@@ -62,7 +69,7 @@ const InstalledPluginFilters = (props: InstalledPluginFilterProps) => {
item
sx={{ alignItems: 'flex-end', display: 'flex' }}>
@@ -71,7 +78,7 @@ const InstalledPluginFilters = (props: InstalledPluginFilterProps) => {
item
sx={{ alignItems: 'flex-end', display: 'flex' }}>
diff --git a/monkey/monkey_island/cc/next_ui/src/app/(protected)/plugins/installed/PluginUpgradeButton.tsx b/monkey/monkey_island/cc/next_ui/src/app/(protected)/plugins/installed/PluginUpgradeButton.tsx
new file mode 100644
index 00000000000..51d1f46efdb
--- /dev/null
+++ b/monkey/monkey_island/cc/next_ui/src/app/(protected)/plugins/installed/PluginUpgradeButton.tsx
@@ -0,0 +1,89 @@
+import { GridActionsCellItem } from '@mui/x-data-grid';
+import FileDownloadIcon from '@mui/icons-material/FileDownload';
+import React from 'react';
+import {
+ useGetLatestPluginVersionQuery,
+ useInstallPluginMutation
+} from '@/redux/features/api/agentPlugins/agentPluginEndpoints';
+import MonkeyLoadingIcon from '@/_components/icons/MonkeyLoadingIcon';
+import DownloadDoneIcon from '@mui/icons-material/DownloadDone';
+import { PluginId, PluginInfo } from '@/redux/features/api/agentPlugins/types';
+
+const PluginUpgradeButton = (props: PluginInfo) => {
+ const { pluginType, pluginName, pluginVersion, pluginId } = props;
+
+ const [
+ upgradePlugin,
+ {
+ isLoading: isUpgrading,
+ isSuccess: isUpgradeSuccessful,
+ reset: resetUpgradePlugin
+ }
+ ] = useInstallPluginMutation({ fixedCacheKey: pluginName + pluginType });
+ const { data: latestPluginVersion, isLoading: isLoadingLatestVersion } =
+ useGetLatestPluginVersionQuery({
+ pluginType: pluginType,
+ pluginName: pluginName
+ });
+
+ const isUpgradable = React.useMemo(() => {
+ if (!latestPluginVersion) {
+ return false;
+ }
+ const upgradeAvailable = latestPluginVersion !== pluginVersion;
+ upgradeAvailable && isUpgradeSuccessful && resetUpgradePlugin();
+ return upgradeAvailable;
+ }, [latestPluginVersion, pluginVersion]);
+
+ const onUpgradeClick = () => {
+ upgradePlugin({
+ pluginVersion: String(latestPluginVersion),
+ pluginName: pluginName,
+ pluginType: pluginType,
+ pluginId: pluginId
+ });
+ };
+
+ if (isUpgrading) {
+ return UpgradeInProgressButton(pluginId);
+ } else if (isUpgradeSuccessful) {
+ return UpgradeDoneButton(pluginId);
+ } else if (isLoadingLatestVersion || !isUpgradable) {
+ return;
+ } else {
+ return UpgradeButton(pluginId, onUpgradeClick);
+ }
+};
+
+const UpgradeButton = (pluginId: PluginId, onUpgradeClick: () => void) => {
+ return (
+ }
+ label="Download"
+ onClick={onUpgradeClick}
+ />
+ );
+};
+
+const UpgradeInProgressButton = (pluginId: PluginId) => {
+ return (
+ }
+ label="Upgrading"
+ />
+ );
+};
+
+const UpgradeDoneButton = (pluginId: PluginId) => {
+ return (
+ }
+ label="Upgrade Complete"
+ />
+ );
+};
+
+export default PluginUpgradeButton;
diff --git a/monkey/monkey_island/cc/next_ui/src/app/(protected)/plugins/installed/page.tsx b/monkey/monkey_island/cc/next_ui/src/app/(protected)/plugins/installed/page.tsx
index 87f46417322..262c73e850a 100644
--- a/monkey/monkey_island/cc/next_ui/src/app/(protected)/plugins/installed/page.tsx
+++ b/monkey/monkey_island/cc/next_ui/src/app/(protected)/plugins/installed/page.tsx
@@ -9,6 +9,7 @@ import PluginTable, {
import Grid from '@mui/material/Grid';
import { InstalledPlugin } from '@/redux/features/api/agentPlugins/types';
import InstalledPluginFilters from '@/app/(protected)/plugins/installed/InstalledPluginFilters';
+import PluginUpgradeButton from '@/app/(protected)/plugins/installed/PluginUpgradeButton';
export default function InstalledPluginsPage() {
const {
@@ -19,9 +20,15 @@ export default function InstalledPluginsPage() {
const [displayedRows, setDisplayedRows] = React.useState([]);
const [isLoadingRows, setIsLoadingRows] = React.useState(false);
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
const getUpgradeAction = (plugin: InstalledPlugin) => {
- return [];
+ return (
+
+ );
};
// eslint-disable-next-line @typescript-eslint/no-unused-vars
@@ -35,7 +42,7 @@ export default function InstalledPluginsPage() {
(installedPlugin) => installedPlugin.id === row.id
);
if (!plugin) return [];
- return [...getUpgradeAction(plugin), ...getUninstallAction(plugin)];
+ return [getUpgradeAction(plugin)];
};
const getOverlayMessage = () => {
diff --git a/monkey/monkey_island/cc/next_ui/src/app/(protected)/plugins/upload/page.tsx b/monkey/monkey_island/cc/next_ui/src/app/(protected)/plugins/upload/page.tsx
index ffb269b6dea..5abb4005e6b 100644
--- a/monkey/monkey_island/cc/next_ui/src/app/(protected)/plugins/upload/page.tsx
+++ b/monkey/monkey_island/cc/next_ui/src/app/(protected)/plugins/upload/page.tsx
@@ -13,14 +13,15 @@ import MonkeyFileUpload, {
} from '@/_components/file-upload/MonkeyFileUpload';
import { useUploadPluginMutation } from '@/redux/features/api/agentPlugins/agentPluginEndpoints';
import MonkeyAlert from '@/_components/alerts/MonkeyAlert';
+import { Severity } from '@/_components/lib/severity';
const UploadNewPlugin = () => {
const [upload, { isError, error, isLoading, isSuccess }] =
useUploadPluginMutation();
- const [plugin, setPlugin] = useState(null);
+ const [plugin, setPlugin] = useState(null);
const [showSuccessAlert, setShowSuccessAlert] = useState(false);
const [pluginName, setPluginName] = useState('');
- const [errors, setErrors] = useState([]);
+ const [errors, setErrors] = useState([]);
const uploadStatus = useMemo(() => {
if (plugin !== null) {
@@ -56,8 +57,10 @@ const UploadNewPlugin = () => {
if (acceptedPlugin?.length) {
const reader = new FileReader();
reader.onload = (e) => {
- if (e.target.readyState === FileReader.DONE) {
- const binaryPlugin = new Uint8Array(e.target.result);
+ if (e.target?.readyState === FileReader.DONE) {
+ const binaryPlugin = new Uint8Array(
+ e.target.result as ArrayBuffer
+ );
setPlugin(binaryPlugin);
setPluginName(Object.assign(acceptedPlugin?.[0]).name);
}
@@ -123,7 +126,7 @@ const UploadNewPlugin = () => {
{showSuccessAlert && (
setShowSuccessAlert(false)}>
'{pluginName}' was successfully installed!
@@ -131,7 +134,9 @@ const UploadNewPlugin = () => {
)}
{showErrors && (
- setErrors([])}>
+ setErrors([])}>
Error uploading Plugin Tar
{errors.map((error, index) => (
diff --git a/monkey/monkey_island/cc/next_ui/src/redux/features/api/agentPlugins/agentPluginEndpoints.tsx b/monkey/monkey_island/cc/next_ui/src/redux/features/api/agentPlugins/agentPluginEndpoints.tsx
index c481ee6540c..2500e8f67c0 100644
--- a/monkey/monkey_island/cc/next_ui/src/redux/features/api/agentPlugins/agentPluginEndpoints.tsx
+++ b/monkey/monkey_island/cc/next_ui/src/redux/features/api/agentPlugins/agentPluginEndpoints.tsx
@@ -10,6 +10,7 @@ import {
PluginTar
} from '@/redux/features/api/agentPlugins/types';
import {
+ parsePluginFromResponse,
parsePluginManifestResponse,
parsePluginMetadataResponse
} from '@/redux/features/api/agentPlugins/responseParsers';
@@ -34,6 +35,27 @@ export const agentPluginEndpoints = islandApiSlice.injectEndpoints({
return parsePluginMetadataResponse(response.plugins);
}
}),
+ getLatestPluginVersion: builder.query<
+ string,
+ { pluginType: string; pluginName: string }
+ >({
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ query: ({ pluginType, pluginName }) => ({
+ url: BackendEndpoints.PLUGIN_INDEX,
+ method: HTTP_METHODS.GET
+ }),
+ transformResponse: (
+ response: {
+ plugins: PluginMetadataResponse;
+ },
+ _,
+ { pluginType, pluginName }
+ ): string => {
+ const pluginFromResponse =
+ response.plugins[pluginType][pluginName].slice(-1)[0];
+ return parsePluginFromResponse(pluginFromResponse)['version'];
+ }
+ }),
getInstalledPlugins: builder.query({
query: () => ({
url: BackendEndpoints.PLUGIN_MANIFESTS,
@@ -43,7 +65,8 @@ export const agentPluginEndpoints = islandApiSlice.injectEndpoints({
response: PluginManifestResponse
): InstalledPlugin[] => {
return parsePluginManifestResponse(response);
- }
+ },
+ providesTags: ['InstalledAgentPlugins']
}),
installPlugin: builder.mutation({
query: (pluginInfo: PluginInfo) => ({
@@ -54,7 +77,8 @@ export const agentPluginEndpoints = islandApiSlice.injectEndpoints({
name: pluginInfo.pluginName,
version: pluginInfo.pluginVersion
}
- })
+ }),
+ invalidatesTags: ['InstalledAgentPlugins']
}),
uploadPlugin: builder.mutation({
query: (pluginTar: PluginTar) => ({
@@ -62,7 +86,20 @@ export const agentPluginEndpoints = islandApiSlice.injectEndpoints({
method: HTTP_METHODS.PUT,
headers: { 'Content-Type': 'application/octet-stream' },
body: pluginTar
- })
+ }),
+ invalidatesTags: ['InstalledAgentPlugins']
+ }),
+ upgradePlugin: builder.mutation({
+ query: (pluginInfo: PluginInfo) => ({
+ url: BackendEndpoints.PLUGIN_INSTALL,
+ method: HTTP_METHODS.PUT,
+ body: {
+ plugin_type: pluginInfo.pluginType,
+ name: pluginInfo.pluginName,
+ version: pluginInfo.pluginVersion
+ }
+ }),
+ invalidatesTags: ['InstalledAgentPlugins']
})
})
});
@@ -71,5 +108,6 @@ export const {
useGetAvailablePluginsQuery,
useGetInstalledPluginsQuery,
useInstallPluginMutation,
- useUploadPluginMutation
+ useUploadPluginMutation,
+ useGetLatestPluginVersionQuery
} = agentPluginEndpoints;
diff --git a/monkey/monkey_island/cc/next_ui/src/redux/features/api/agentPlugins/responseParsers.ts b/monkey/monkey_island/cc/next_ui/src/redux/features/api/agentPlugins/responseParsers.ts
index 69e2af8766b..d44e6a6e6ab 100644
--- a/monkey/monkey_island/cc/next_ui/src/redux/features/api/agentPlugins/responseParsers.ts
+++ b/monkey/monkey_island/cc/next_ui/src/redux/features/api/agentPlugins/responseParsers.ts
@@ -29,7 +29,7 @@ export const parsePluginMetadataResponse = (
return plugins;
};
-const parsePluginFromResponse = (
+export const parsePluginFromResponse = (
unparsedPlugin: PluginMetadata
): AvailablePlugin => {
return {
diff --git a/monkey/monkey_island/cc/next_ui/src/redux/features/api/agentPlugins/types.ts b/monkey/monkey_island/cc/next_ui/src/redux/features/api/agentPlugins/types.ts
index 066b98453c6..c27db46bc25 100644
--- a/monkey/monkey_island/cc/next_ui/src/redux/features/api/agentPlugins/types.ts
+++ b/monkey/monkey_island/cc/next_ui/src/redux/features/api/agentPlugins/types.ts
@@ -1,5 +1,7 @@
+export type PluginId = string;
+
export type AgentPlugin = {
- id: string;
+ id: PluginId;
name: string;
pluginType: string;
description: string;
@@ -55,7 +57,7 @@ export type PluginInfo = {
pluginType: string;
pluginName: string;
pluginVersion: string;
- pluginId: string;
+ pluginId: PluginId;
};
export type PluginTar = Uint8Array;
diff --git a/monkey/monkey_island/cc/next_ui/src/redux/features/api/islandApiSlice.ts b/monkey/monkey_island/cc/next_ui/src/redux/features/api/islandApiSlice.ts
index e4fc356ec42..eb7233d7855 100644
--- a/monkey/monkey_island/cc/next_ui/src/redux/features/api/islandApiSlice.ts
+++ b/monkey/monkey_island/cc/next_ui/src/redux/features/api/islandApiSlice.ts
@@ -49,6 +49,7 @@ const getIslandBaseQuery = async (
export const islandApiSlice = createApi({
reducerPath: 'islandApi',
+ tagTypes: ['InstalledAgentPlugins'],
baseQuery: getIslandBaseQuery,
endpoints: () => ({})
});