Skip to content

Commit 314cd0f

Browse files
committed
frontend: PluginSettings: Rework fetching and storing plugin information
This PR reduces the size of plugin information saved in local storage, it also introduces the getPlugins hook for fetching plugin information. Signed-off-by: Vincent T <[email protected]>
1 parent 3a0f1df commit 314cd0f

File tree

9 files changed

+172
-120
lines changed

9 files changed

+172
-120
lines changed

frontend/src/components/App/PluginSettings/PluginSettings.tsx

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { useEffect, useState } from 'react';
77
import { useTranslation } from 'react-i18next';
88
import { useDispatch } from 'react-redux';
99
import { isElectron } from '../../../helpers/isElectron';
10+
import { usePlugins } from '../../../lib/k8s/api/v2/plugins';
1011
import { useFilterFunc } from '../../../lib/util';
1112
import { PluginInfo, reloadPage, setPluginSettings } from '../../../plugin/pluginsSlice';
1213
import { useTypedSelector } from '../../../redux/reducers/reducers';
@@ -29,7 +30,7 @@ export interface PluginSettingsPureProps {
2930
}
3031

3132
/** PluginSettingsProp intentially left empty to remain malleable */
32-
export interface PluginSettingsProps {}
33+
export interface PluginSettingsProps { }
3334

3435
const EnableSwitch = (props: SwitchProps) => {
3536
const theme = useTheme();
@@ -285,9 +286,15 @@ export default function PluginSettings() {
285286

286287
const pluginSettings = useTypedSelector(state => state.plugins.pluginSettings);
287288

289+
const { data: plugins } = usePlugins(pluginSettings);
290+
291+
if (!plugins?.length) {
292+
return null;
293+
}
294+
288295
return (
289296
<PluginSettingsPure
290-
plugins={pluginSettings}
297+
plugins={plugins}
291298
onSave={plugins => {
292299
dispatch(setPluginSettings(plugins));
293300
dispatch(reloadPage());

frontend/src/components/App/PluginSettings/PluginSettingsDetails.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { useParams } from 'react-router-dom';
99
import { useHistory } from 'react-router-dom';
1010
import { isElectron } from '../../../helpers/isElectron';
1111
import { getCluster } from '../../../lib/cluster';
12+
import { usePlugins } from '../../../lib/k8s/api/v2/plugins';
1213
import { deletePlugin } from '../../../lib/k8s/apiProxy';
1314
import { ConfigStore } from '../../../plugin/configStore';
1415
import { PluginInfo, reloadPage } from '../../../plugin/pluginsSlice';
@@ -66,13 +67,14 @@ const PluginSettingsDetailsInitializer = (props: { plugin: PluginInfo }) => {
6667
};
6768

6869
export default function PluginSettingsDetails() {
69-
const pluginSettings = useTypedSelector(state => state.plugins.pluginSettings);
7070
const { name } = useParams<{ name: string }>();
71+
const pluginSettings = useTypedSelector(state => state.plugins.pluginSettings);
72+
const { data: plugins } = usePlugins(pluginSettings);
7173

7274
const plugin = useMemo(() => {
7375
const decodedName = decodeURIComponent(name);
74-
return pluginSettings.find(plugin => plugin.name === decodedName);
75-
}, [pluginSettings, name]);
76+
return plugins?.find((plugin: PluginInfo) => plugin.name === decodedName);
77+
}, [plugins, name]);
7678

7779
if (!plugin) {
7880
return <NotFoundComponent />;

frontend/src/lib/k8s/api/v2/fetch.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ export const BASE_HTTP_URL = getAppUrl();
1717
*
1818
* @returns fetch Response
1919
*/
20-
async function backendFetch(url: string | URL, init: RequestInit) {
20+
export async function backendFetch(url: string | URL, init?: RequestInit) {
2121
const response = await fetch(makeUrl([BASE_HTTP_URL, url]), init);
2222

2323
// The backend signals through this header that it wants a reload.
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import '@tanstack/react-query';
2+
import { useQuery } from '@tanstack/react-query';
3+
import semver from 'semver';
4+
import helpers from '../../../../helpers';
5+
import { PluginInfo, PluginSettings } from '../../../../plugin/pluginsSlice';
6+
import { backendFetch } from './fetch';
7+
8+
/*
9+
* this function is used to check the compatibility of the plugin version with the headlamp version
10+
*
11+
* @param compatibleVersion headlamp-plugin version this build is compatible with.
12+
* If the plugin engine version is not compatible, the plugin will not be loaded.
13+
* Can be set to a semver range, e.g. '>= 0.6.0' or '0.6.0 - 0.7.0'.
14+
* If set to an empty string, all plugin versions will be loaded.
15+
*/
16+
export function checkCompatibleVersion(packageInfo: PluginInfo, checkAllVersions: boolean = false) {
17+
/*
18+
* this is the compatible version of the plugin with the headlamp version
19+
*/
20+
const compatibleVersion = !checkAllVersions ? '>=0.8.0-alpha.3' : '';
21+
22+
// Can set this to a semver version range like '>=0.8.0-alpha.3'.
23+
// '' means all versions.
24+
const isCompatible = semver.satisfies(
25+
semver.coerce(packageInfo.devDependencies?.['@kinvolk/headlamp-plugin']) || '',
26+
compatibleVersion
27+
);
28+
29+
return isCompatible;
30+
}
31+
32+
export async function getPluginPaths() {
33+
const pluginPaths = (await fetch(`${helpers.getAppUrl()}plugins`).then(resp =>
34+
resp.json()
35+
)) as string[];
36+
37+
return pluginPaths;
38+
}
39+
40+
/*
41+
* this function is used for the plugin settings and is extended from the original `fetchAndExecutePlugins` function in the Plugins.tsx file
42+
* - the function is used to fetch the plugins from the backend and return the plugins with their settings
43+
* - it will also do a compatibility check for the plugins and return the plugins with their compatibility status,
44+
* - the compatibility check is needed to render the plugin switches
45+
*/
46+
export async function getPlugins(pluginSettings: PluginSettings[]) {
47+
const pluginPaths = await getPluginPaths();
48+
49+
const packageInfosPromise = await Promise.all<PluginInfo>(
50+
pluginPaths.map(path =>
51+
backendFetch(`${path}/package.json`).then(resp => {
52+
if (!resp.ok) {
53+
if (resp.status !== 404) {
54+
return Promise.reject(resp);
55+
}
56+
57+
console.warn(
58+
'Missing package.json. ' +
59+
`Please upgrade the plugin ${path}` +
60+
' by running "headlamp-plugin extract" again.' +
61+
' Please use headlamp-plugin >= 0.8.0'
62+
);
63+
64+
return {
65+
name: path.split('/').slice(-1)[0],
66+
version: '0.0.0',
67+
author: 'unknown',
68+
description: '',
69+
};
70+
}
71+
return resp.json();
72+
})
73+
)
74+
);
75+
76+
const packageInfos = await packageInfosPromise;
77+
78+
const pluginsWithIsEnabled = packageInfos.map(plugin => {
79+
const matchedSetting = pluginSettings.find(p => plugin.name === p.name);
80+
if (matchedSetting) {
81+
const isCompatible = checkCompatibleVersion(plugin);
82+
83+
return {
84+
...plugin,
85+
settingsComponent: matchedSetting.settingsComponent,
86+
displaySettingsComponentWithSaveButton:
87+
matchedSetting.displaySettingsComponentWithSaveButton,
88+
isEnabled: matchedSetting.isEnabled,
89+
isCompatible: isCompatible,
90+
};
91+
}
92+
return plugin;
93+
});
94+
95+
return pluginsWithIsEnabled;
96+
}
97+
98+
export function usePlugins(pluginSettings: { name: string; isEnabled: boolean }[]) {
99+
// takes two params, the key and the function that will be called to get the data
100+
return useQuery({ queryKey: ['plugins'], queryFn: () => getPlugins(pluginSettings) });
101+
}

frontend/src/plugin/filterSources.test.ts

Lines changed: 5 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,7 @@ describe('filterSources', () => {
88
const settingsPackages = undefined;
99
const appMode = false;
1010

11-
const { sourcesToExecute } = filterSources(
12-
sources,
13-
packageInfos,
14-
appMode,
15-
'>=0.8.0-alpha.3',
16-
settingsPackages
17-
);
11+
const { sourcesToExecute } = filterSources(sources, packageInfos, appMode, settingsPackages);
1812
expect(sourcesToExecute.length).toBe(0);
1913
});
2014

@@ -39,7 +33,6 @@ describe('filterSources', () => {
3933
sources,
4034
packageInfos,
4135
appMode,
42-
'>=0.8.0-alpha.3',
4336
settingsPackages
4437
);
4538
expect(Object.keys(incompatiblePlugins).length).toBe(0);
@@ -68,13 +61,7 @@ describe('filterSources', () => {
6861
},
6962
];
7063
const appMode = true;
71-
const { sourcesToExecute } = filterSources(
72-
sources,
73-
packageInfos,
74-
appMode,
75-
'>=0.8.0-alpha.3',
76-
settingsPackages
77-
);
64+
const { sourcesToExecute } = filterSources(sources, packageInfos, appMode, settingsPackages);
7865

7966
expect(sourcesToExecute.length).toBe(0);
8067
});
@@ -122,13 +109,7 @@ describe('filterSources', () => {
122109
},
123110
];
124111
const appMode = true;
125-
const { sourcesToExecute } = filterSources(
126-
sources,
127-
packageInfos,
128-
appMode,
129-
'>=0.8.0-alpha.3',
130-
settingsPackages
131-
);
112+
const { sourcesToExecute } = filterSources(sources, packageInfos, appMode, settingsPackages);
132113

133114
expect(sourcesToExecute.length).toBe(1);
134115
expect(sourcesToExecute[0]).toBe('source1');
@@ -181,7 +162,6 @@ describe('filterSources', () => {
181162
sources,
182163
packageInfos,
183164
appMode,
184-
'>=0.8.0-alpha.3',
185165
settingsPackages
186166
);
187167

@@ -194,8 +174,8 @@ describe('filterSources', () => {
194174
sources,
195175
packageInfos,
196176
appMode,
197-
'', // empty string disables compatibility check
198-
settingsPackages
177+
settingsPackages,
178+
true
199179
);
200180

201181
expect(disabledCompatCheck.sourcesToExecute.length).toBe(2);

frontend/src/plugin/index.ts

Lines changed: 18 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,11 @@ import * as ReactDOM from 'react-dom';
1818
import * as ReactRedux from 'react-redux';
1919
import * as ReactRouter from 'react-router-dom';
2020
import * as Recharts from 'recharts';
21-
import semver from 'semver';
2221
import * as CommonComponents from '../components/common';
2322
import { getAppUrl } from '../helpers/getAppUrl';
2423
import { isElectron } from '../helpers/isElectron';
2524
import * as K8s from '../lib/k8s';
25+
import { checkCompatibleVersion, getPluginPaths, getPlugins } from '../lib/k8s/api/v2/plugins';
2626
import * as ApiProxy from '../lib/k8s/apiProxy';
2727
import * as Crd from '../lib/k8s/crd';
2828
import * as Notification from '../lib/notification';
@@ -126,10 +126,6 @@ export async function initializePlugins() {
126126
* @param sources array of source to execute. Has the same order as packageInfos.
127127
* @param packageInfos array of package.json contents
128128
* @param appMode if we are in app mode
129-
* @param compatibleVersion headlamp-plugin version this build is compatible with.
130-
* If the plugin engine version is not compatible, the plugin will not be loaded.
131-
* Can be set to a semver range, e.g. '>= 0.6.0' or '0.6.0 - 0.7.0'.
132-
* If set to an empty string, all plugin versions will be loaded.
133129
* @param settingsPackages the packages from settings
134130
*
135131
* @returns the sources to execute and incompatible PackageInfos
@@ -139,8 +135,8 @@ export function filterSources(
139135
sources: string[],
140136
packageInfos: PluginInfo[],
141137
appMode: boolean,
142-
compatibleVersion: string,
143-
settingsPackages?: PluginInfo[]
138+
settingsPackages?: PluginInfo[],
139+
disableCompatibilityCheck?: boolean
144140
) {
145141
const incompatiblePlugins: Record<string, PluginInfo> = {};
146142

@@ -168,11 +164,11 @@ export function filterSources(
168164
return enabledInSettings;
169165
});
170166

167+
const checkAllVersion = disableCompatibilityCheck;
168+
171169
const compatible = enabledSourcesAndPackageInfos.filter(({ packageInfo }) => {
172-
const isCompatible = semver.satisfies(
173-
semver.coerce(packageInfo.devDependencies?.['@kinvolk/headlamp-plugin']) || '',
174-
compatibleVersion
175-
);
170+
const isCompatible = checkCompatibleVersion(packageInfo, checkAllVersion);
171+
176172
if (!isCompatible) {
177173
incompatiblePlugins[packageInfo.name] = packageInfo;
178174
return false;
@@ -200,17 +196,21 @@ export function filterSources(
200196
*/
201197
export function updateSettingsPackages(
202198
backendPlugins: PluginInfo[],
203-
settingsPlugins: PluginInfo[]
199+
settingsPlugins: { name: string; isEnabled: boolean }[]
204200
): PluginInfo[] {
205201
if (backendPlugins.length === 0) return [];
206202

207203
const pluginsChanged =
208204
backendPlugins.length !== settingsPlugins.length ||
209-
backendPlugins.map(p => p.name + p.version).join('') !==
210-
settingsPlugins.map(p => p.name + p.version).join('');
205+
JSON.stringify(backendPlugins.map(p => p.name).sort()) !==
206+
JSON.stringify(settingsPlugins.map(p => p.name).sort());
211207

212208
if (!pluginsChanged) {
213-
return settingsPlugins;
209+
const updatedPlugins = backendPlugins.filter(plugin =>
210+
settingsPlugins.some(setting => setting.name === plugin.name)
211+
);
212+
213+
return updatedPlugins;
214214
}
215215

216216
return backendPlugins.map(plugin => {
@@ -242,42 +242,17 @@ export function updateSettingsPackages(
242242
*
243243
*/
244244
export async function fetchAndExecutePlugins(
245-
settingsPackages: PluginInfo[],
245+
settingsPackages: { name: string; isEnabled: boolean }[],
246246
onSettingsChange: (plugins: PluginInfo[]) => void,
247247
onIncompatible: (plugins: Record<string, PluginInfo>) => void
248248
) {
249-
const pluginPaths = (await fetch(`${getAppUrl()}plugins`).then(resp => resp.json())) as string[];
249+
const pluginPaths = await getPluginPaths();
250250

251251
const sourcesPromise = Promise.all(
252252
pluginPaths.map(path => fetch(`${getAppUrl()}${path}/main.js`).then(resp => resp.text()))
253253
);
254254

255-
const packageInfosPromise = await Promise.all<PluginInfo>(
256-
pluginPaths.map(path =>
257-
fetch(`${getAppUrl()}${path}/package.json`).then(resp => {
258-
if (!resp.ok) {
259-
if (resp.status !== 404) {
260-
return Promise.reject(resp);
261-
}
262-
{
263-
console.warn(
264-
'Missing package.json. ' +
265-
`Please upgrade the plugin ${path}` +
266-
' by running "headlamp-plugin extract" again.' +
267-
' Please use headlamp-plugin >= 0.8.0'
268-
);
269-
return {
270-
name: path.split('/').slice(-1)[0],
271-
version: '0.0.0',
272-
author: 'unknown',
273-
description: '',
274-
};
275-
}
276-
}
277-
return resp.json();
278-
})
279-
)
280-
);
255+
const packageInfosPromise = await getPlugins(settingsPackages);
281256

282257
const sources = await sourcesPromise;
283258
const packageInfos = await packageInfosPromise;
@@ -288,15 +263,10 @@ export async function fetchAndExecutePlugins(
288263
onSettingsChange(updatedSettingsPackages);
289264
}
290265

291-
// Can set this to a semver version range like '>=0.8.0-alpha.3'.
292-
// '' means all versions.
293-
const compatibleHeadlampPluginVersion = '>=0.8.0-alpha.3';
294-
295266
const { sourcesToExecute, incompatiblePlugins } = filterSources(
296267
sources,
297268
packageInfos,
298269
isElectron(),
299-
compatibleHeadlampPluginVersion,
300270
updatedSettingsPackages
301271
);
302272

0 commit comments

Comments
 (0)