Skip to content

Commit 1cab03a

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 25372f6 commit 1cab03a

File tree

9 files changed

+171
-121
lines changed

9 files changed

+171
-121
lines changed

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

+8-1
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 helpers from '../../../helpers';
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';
@@ -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

+5-3
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 helpers from '../../../helpers';
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

+1-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ export const BASE_HTTP_URL = helpers.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.
+101
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

+5-25
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

+18-50
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,10 @@ 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 helpers from '../helpers';
2423
import * as K8s from '../lib/k8s';
24+
import { checkCompatibleVersion, getPluginPaths, getPlugins } from '../lib/k8s/api/v2/plugins';
2525
import * as ApiProxy from '../lib/k8s/apiProxy';
2626
import * as Crd from '../lib/k8s/crd';
2727
import * as Notification from '../lib/notification';
@@ -125,10 +125,6 @@ export async function initializePlugins() {
125125
* @param sources array of source to execute. Has the same order as packageInfos.
126126
* @param packageInfos array of package.json contents
127127
* @param appMode if we are in app mode
128-
* @param compatibleVersion headlamp-plugin version this build is compatible with.
129-
* If the plugin engine version is not compatible, the plugin will not be loaded.
130-
* Can be set to a semver range, e.g. '>= 0.6.0' or '0.6.0 - 0.7.0'.
131-
* If set to an empty string, all plugin versions will be loaded.
132128
* @param settingsPackages the packages from settings
133129
*
134130
* @returns the sources to execute and incompatible PackageInfos
@@ -138,8 +134,8 @@ export function filterSources(
138134
sources: string[],
139135
packageInfos: PluginInfo[],
140136
appMode: boolean,
141-
compatibleVersion: string,
142-
settingsPackages?: PluginInfo[]
137+
settingsPackages?: PluginInfo[],
138+
disableCompatibilityCheck?: boolean
143139
) {
144140
const incompatiblePlugins: Record<string, PluginInfo> = {};
145141

@@ -167,11 +163,11 @@ export function filterSources(
167163
return enabledInSettings;
168164
});
169165

166+
const checkAllVersion = disableCompatibilityCheck;
167+
170168
const compatible = enabledSourcesAndPackageInfos.filter(({ packageInfo }) => {
171-
const isCompatible = semver.satisfies(
172-
semver.coerce(packageInfo.devDependencies?.['@kinvolk/headlamp-plugin']) || '',
173-
compatibleVersion
174-
);
169+
const isCompatible = checkCompatibleVersion(packageInfo, checkAllVersion);
170+
175171
if (!isCompatible) {
176172
incompatiblePlugins[packageInfo.name] = packageInfo;
177173
return false;
@@ -199,17 +195,21 @@ export function filterSources(
199195
*/
200196
export function updateSettingsPackages(
201197
backendPlugins: PluginInfo[],
202-
settingsPlugins: PluginInfo[]
198+
settingsPlugins: { name: string; isEnabled: boolean }[]
203199
): PluginInfo[] {
204200
if (backendPlugins.length === 0) return [];
205201

206202
const pluginsChanged =
207203
backendPlugins.length !== settingsPlugins.length ||
208-
backendPlugins.map(p => p.name + p.version).join('') !==
209-
settingsPlugins.map(p => p.name + p.version).join('');
204+
JSON.stringify(backendPlugins.map(p => p.name).sort()) !==
205+
JSON.stringify(settingsPlugins.map(p => p.name).sort());
210206

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

215215
return backendPlugins.map(plugin => {
@@ -241,46 +241,19 @@ export function updateSettingsPackages(
241241
*
242242
*/
243243
export async function fetchAndExecutePlugins(
244-
settingsPackages: PluginInfo[],
244+
settingsPackages: { name: string; isEnabled: boolean }[],
245245
onSettingsChange: (plugins: PluginInfo[]) => void,
246246
onIncompatible: (plugins: Record<string, PluginInfo>) => void
247247
) {
248-
const pluginPaths = (await fetch(`${helpers.getAppUrl()}plugins`).then(resp =>
249-
resp.json()
250-
)) as string[];
248+
const pluginPaths = await getPluginPaths();
251249

252250
const sourcesPromise = Promise.all(
253251
pluginPaths.map(path =>
254252
fetch(`${helpers.getAppUrl()}${path}/main.js`).then(resp => resp.text())
255253
)
256254
);
257255

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

285258
const sources = await sourcesPromise;
286259
const packageInfos = await packageInfosPromise;
@@ -291,15 +264,10 @@ export async function fetchAndExecutePlugins(
291264
onSettingsChange(updatedSettingsPackages);
292265
}
293266

294-
// Can set this to a semver version range like '>=0.8.0-alpha.3'.
295-
// '' means all versions.
296-
const compatibleHeadlampPluginVersion = '>=0.8.0-alpha.3';
297-
298267
const { sourcesToExecute, incompatiblePlugins } = filterSources(
299268
sources,
300269
packageInfos,
301270
helpers.isElectron(),
302-
compatibleHeadlampPluginVersion,
303271
updatedSettingsPackages
304272
);
305273

0 commit comments

Comments
 (0)