plugins: Ship plugin-catalog with an AKS Desktop optimized filter - #848
Conversation
There was a problem hiding this comment.
Pull request overview
This PR vendors the upstream Headlamp plugin-catalog plugin into plugins/plugin-catalog, integrates it into the AKS Desktop build so it ships in the binary, and replaces the “only official/verified” filtering concept with an “AKS Desktop optimized” toggle backed by a curated allowlist JSON.
Changes:
- Add a vendored
plugins/plugin-catalogplugin with catalog, detail, and installed-plugins views. - Introduce an “AKS Desktop optimized” filter driven by
src/aks-optimized-plugins.jsonand persisted via plugin settings. - Bundle
plugin-catalogduring packaging by adding it tobuild/setup-plugins.ts.
Reviewed changes
Copilot reviewed 43 out of 46 changed files in this pull request and generated 8 comments.
Show a summary per file
| File | Description |
|---|---|
| build/setup-plugins.ts | Adds plugin-catalog to the set of plugins built and bundled into AKS Desktop. |
| plugins/plugin-catalog/.gitignore | Plugin-local ignore rules (node_modules, dist, storybook build output, etc.). |
| plugins/plugin-catalog/README.md | Plugin README (dev instructions and links to Headlamp docs). |
| plugins/plugin-catalog/package.json | Declares plugin package metadata, scripts, i18n languages list, and deps. |
| plugins/plugin-catalog/tsconfig.json | Plugin TypeScript configuration (JSON imports enabled). |
| plugins/plugin-catalog/src/storybook.test.tsx | Storyshot test runner for *.stories.tsx. |
| plugins/plugin-catalog/src/index.tsx | Registers sidebar entries/routes and plugin settings for the catalog. |
| plugins/plugin-catalog/src/headlamp-plugin.d.ts | Adjusts *.svg typing to match the build’s SVGR behavior. |
| plugins/plugin-catalog/src/aks-optimized-plugins.json | Curated allowlist for the “AKS Desktop optimized” filter. |
| plugins/plugin-catalog/src/components/plugins/Settings.tsx | Plugin settings UI for the optimized filter setting. |
| plugins/plugin-catalog/src/components/plugins/PluginCard.tsx | Plugin catalog card UI (logo, badges, description truncation). |
| plugins/plugin-catalog/src/components/plugins/PluginCard.stories.tsx | Storybook stories for PluginCard. |
| plugins/plugin-catalog/src/components/plugins/plugin-icon.svg | Default icon for plugins without ArtifactHub logos. |
| plugins/plugin-catalog/src/components/plugins/LoadingButton.tsx | Loading/progress UI used during install/update/uninstall operations. |
| plugins/plugin-catalog/src/components/plugins/LoadingButton.stories.tsx | Storybook stories for LoadingButton. |
| plugins/plugin-catalog/src/components/plugins/List.tsx | Catalog list page, ArtifactHub fetching, optimized allowlist filter toggle. |
| plugins/plugin-catalog/src/components/plugins/List.stories.tsx | Storybook stories for the catalog list UI. |
| plugins/plugin-catalog/src/components/plugins/InstalledList.tsx | Lists installed plugins (catalog vs non-catalog). |
| plugins/plugin-catalog/src/components/plugins/InstalledList.stories.tsx | Storybook stories for installed plugins list UI. |
| plugins/plugin-catalog/src/components/plugins/Detail.tsx | Plugin detail page + install/update/uninstall actions and version selection. |
| plugins/plugin-catalog/src/components/plugins/Detail.stories.tsx | Storybook stories for plugin detail UI states. |
| plugins/plugin-catalog/src/components/plugins/snapshots/PluginCard.stories.storyshot | Stored storyshot snapshots for PluginCard. |
| plugins/plugin-catalog/src/components/plugins/snapshots/LoadingButton.stories.storyshot | Stored storyshot snapshots for LoadingButton. |
| plugins/plugin-catalog/src/components/plugins/snapshots/List.stories.storyshot | Stored storyshot snapshots for list page stories. |
| plugins/plugin-catalog/src/components/plugins/snapshots/InstalledList.stories.storyshot | Stored storyshot snapshots for installed list stories. |
| plugins/plugin-catalog/src/components/plugins/snapshots/Detail.stories.storyshot | Stored storyshot snapshots for detail page stories. |
| plugins/plugin-catalog/locales/en/translation.json | English translations for plugin-catalog UI strings. |
| plugins/plugin-catalog/locales/de/translation.json | German translation placeholder file for plugin-catalog. |
| plugins/plugin-catalog/locales/es/translation.json | Spanish translation placeholder file for plugin-catalog. |
| plugins/plugin-catalog/locales/fr/translation.json | French translation placeholder file for plugin-catalog. |
| plugins/plugin-catalog/locales/it/translation.json | Italian translation placeholder file for plugin-catalog. |
| plugins/plugin-catalog/locales/ja/translation.json | Japanese translation placeholder file for plugin-catalog. |
| plugins/plugin-catalog/locales/id/translation.json | Indonesian translation placeholder file for plugin-catalog. |
| plugins/plugin-catalog/locales/ko/translation.json | Korean translation placeholder file for plugin-catalog. |
| plugins/plugin-catalog/locales/pt-BR/translation.json | Brazilian Portuguese translation placeholder file for plugin-catalog. |
| plugins/plugin-catalog/locales/pt-PT/translation.json | Portuguese (Portugal) translation placeholder file for plugin-catalog. |
| plugins/plugin-catalog/locales/ru/translation.json | Russian translation placeholder file for plugin-catalog. |
| plugins/plugin-catalog/locales/zh-Hans/translation.json | Simplified Chinese translation placeholder file for plugin-catalog. |
| plugins/plugin-catalog/locales/zh-Hant/translation.json | Traditional Chinese translation placeholder file for plugin-catalog. |
| plugins/plugin-catalog/locales/cs/translation.json | Czech translation placeholder file for plugin-catalog. |
| plugins/plugin-catalog/locales/nl/translation.json | Dutch translation placeholder file for plugin-catalog. |
| plugins/plugin-catalog/locales/hu/translation.json | Hungarian translation placeholder file for plugin-catalog. |
| plugins/plugin-catalog/locales/pl/translation.json | Polish translation placeholder file for plugin-catalog. |
| plugins/plugin-catalog/locales/sv/translation.json | Swedish translation placeholder file for plugin-catalog. |
| plugins/plugin-catalog/locales/tr/translation.json | Turkish translation placeholder file for plugin-catalog. |
Suppressed comments (1)
plugins/plugin-catalog/src/components/plugins/List.stories.tsx:117
Loadingstory doesn’t set required args, which makes the rendered output differ from the stored storyshot snapshot (e.g.searchbecomesundefined, changing the input value output). Provide explicit args for the loading state.
export const Loading = Template.bind({});
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 43 out of 46 changed files in this pull request and generated 1 comment.
Suppressed comments (13)
plugins/plugin-catalog/src/components/plugins/Detail.tsx:472
- Changing
currentActiontriggers this initialization effect, which callsfetchStatus()at line 460, and also triggers the second effect, which calls it again. Every install/update/uninstall therefore starts two polling loops that race to update the same state. Initialize only on route changes and keep a single polling effect for actions.
}, [repoName, pluginName, currentAction]);
useEffect(() => {
if (currentAction) {
fetchStatus();
plugins/plugin-catalog/src/components/plugins/InstalledList.tsx:14
folderNameis a plugin folder/package identifier, and this component passes it as thepluginNameroute segment at line 73. Typing it as a boolean permits invalid detail links and contradicts thePluginManager.list()payload being asserted here; it should be a string.
folderName: boolean;
plugins/plugin-catalog/src/components/plugins/InstalledList.stories.tsx:59
- This story still supplies the obsolete
installedPluginsprop, whilePurePluginInstalledListrequirescatalogPluginsandnonCatalogPlugins. Consequently the sample data is ignored (and strict story typings may reject the args), so the Default story does not exercise its intended state.
export const Default = Template.bind({});
Default.args = {
installedPlugins: samplePlugins,
error: null,
};
plugins/plugin-catalog/src/components/plugins/InstalledList.stories.tsx:65
- The error story also uses the removed
installedPluginsprop and omits the current list props, so its args no longer matchPurePluginInstalledListProps.
export const WithError = Template.bind({});
WithError.args = {
installedPlugins: null,
error: 'Failed to load plugins.',
};
plugins/plugin-catalog/src/components/plugins/InstalledList.stories.tsx:71
- Both list props use their old names here, and the non-null
errorprevents either list from rendering anyway. This story therefore cannot demonstrate repeated plugins as named.
export const WithRepeatedPlugins = Template.bind({});
WithRepeatedPlugins.args = {
installedPlugins: samplePlugins,
otherInstalledPlugins: [samplePlugins[1]],
error: 'Failed to load plugins.',
plugins/plugin-catalog/src/components/plugins/InstalledList.stories.tsx:49
- Use the actual string folder identifier here as well;
truewould generate an invalid plugin detail URL once this fixture is passed throughcatalogPlugins.
folderName: true,
plugins/plugin-catalog/src/components/plugins/List.tsx:337
- If any ArtifactHub/proxy request rejects, this effect leaves
allPluginsasnull, so the catalog shows its loading spinner forever and emits an unhandled rejection. Catch the failure and render an error state; also ignore responses after unmount or a newer settings request.
plugins/plugin-catalog/src/components/plugins/List.tsx:386 totalPageswas computed before applyingsearch, so a narrow search still exposes pages from the full catalog; selecting one of them produces an empty result page. Derive the count fromfilteredPluginswhile retaining the fetched count during initial loading.
plugins/plugin-catalog/src/components/plugins/Detail.tsx:457- This functional update retains the version selected for the previous plugin when route parameters change without remounting the component. The Install action can then request that stale version for the newly opened plugin. Reset the selection when
repoName/pluginNamechanges, while separating initialization from action polling so polling does not overwrite a user's choice.
// Default to the latest version, but don't override a version the user
// has already picked (this runs again whenever currentAction changes).
setSelectedVersion(prev => prev || enrichedPluginData.version);
plugins/plugin-catalog/src/components/plugins/Detail.tsx:442
- On a polling failure, the action itself is never cleared. The snackbar appears, but the UI remains in the loading/cancel state indefinitely because
currentActionis still non-null.
} catch (error) {
setCurrentActionState(null);
setCurrentActionMessage(null);
setCurrentActionProgress(0);
setAlertMessage(t('An unexpected error occurred: {{error}}', { error: String(error) }));
plugins/plugin-catalog/package.json:28
- This package declares 19 runtime locales, but
plugin-catalogis absent from both the localization source registry (Localize/translation-manager.mjs:22-37) and CI'sTRANSLATION_PLUGINSlist (.github/workflows/ci.yml:71). The added non-English files are empty and OneLoc will never collect or distribute this plugin's strings. Register this locale directory in both places as done for the AKS and AI plugins.
"headlamp": {
"i18n": [
"en",
plugins/plugin-catalog/README.md:23
- This command targets the standalone
headlamp-k8s/pluginscheckout and does not exist in this repository. Contributors following the vendored README from the AKS Desktop root need the local plugin path.
```bash
cd headlamp-k8s/plugins/plugin-catalog
npm install
npm start
plugins/plugin-catalog/src/components/plugins/InstalledList.stories.tsx:40
- The fixture should model
PluginManager.list()accurately:folderNameis the string used as the plugin detail route segment, not a boolean.
This issue also appears on line 49 of the same file.
folderName: true,
René Dudfield (illume)
left a comment
There was a problem hiding this comment.
Please check the copilot comments after you push?
What happens to the existing CNCF / Official switch and the other third selection?
9c56b8d to
10b4553
Compare
Signed-off-by: yolossn <sannagaraj@microsoft.com>
Add plugin-catalog to the list of plugins that are built and bundled into the AKS Desktop binary. Signed-off-by: yolossn <sannagaraj@microsoft.com>
10b4553 to
e14ac9b
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 46 out of 49 changed files in this pull request and generated no new comments.
Suppressed comments (11)
plugins/plugin-catalog/src/components/plugins/Detail.tsx:443
- If
getStatusthrows, this catch clears the status fields but leavescurrentActionnon-null. The UI therefore remains permanently in its loading-action state and the polling effect will not restart. Clear the active action on this error path.
setCurrentActionState(null);
setCurrentActionMessage(null);
setCurrentActionProgress(0);
setAlertMessage(t('An unexpected error occurred: {{error}}', { error: String(error) }));
plugins/plugin-catalog/src/components/plugins/List.tsx:333
- A proxy/network failure rejects
processPlugins, but this async effect has no error path.allPluginsremains null indefinitely, leaving the catalog on a permanent loading spinner with an unhandled rejection. Catch the failure and render an actionable error/retry state.
plugins/plugin-catalog/src/components/plugins/InstalledList.stories.tsx:65 - This scenario also uses the removed
installedPluginsprop and omits both required list props, so the co-locatednpm run tsccheck rejects the story args. Pass the current component contract even though the error branch hides the tables.
WithError.args = {
installedPlugins: null,
error: 'Failed to load plugins.',
};
plugins/plugin-catalog/src/components/plugins/InstalledList.stories.tsx:72
- Both data prop names here are stale (
installedPlugins/otherInstalledPlugins), and settingerrormeans the repeated-plugin tables would not render anyway. Use the current catalog/non-catalog props and a null error so this story exercises its named scenario.
WithRepeatedPlugins.args = {
installedPlugins: samplePlugins,
otherInstalledPlugins: [samplePlugins[1]],
error: 'Failed to load plugins.',
};
plugins/plugin-catalog/src/components/plugins/InstalledList.stories.tsx:59
PurePluginInstalledListhas noinstalledPluginsprop; it expectscatalogPluginsandnonCatalogPlugins. This story therefore fails typed Storybook args checking and renders empty tables instead of the sample catalog data.
Default.args = {
installedPlugins: samplePlugins,
error: null,
};
plugins/plugin-catalog/src/components/plugins/List.tsx:386
- The pagination count is calculated before applying
search, so a narrow search still shows pages for the full catalog; those extra pages are empty. Derive the count fromfilteredPluginsso pagination matches the displayed result set.
plugins/plugin-catalog/src/components/plugins/List.tsx:394 - Changing back to the optimized catalog keeps the current page. If the user is on page 2+ of the full catalog, the optimized list is sliced at that page and appears empty even though curated plugins exist. Reset pagination when the filter changes.
plugins/plugin-catalog/src/components/plugins/LoadingButton.tsx:39 - Real operations keep
currentActionProgressat 0 becausePluginManagerexposes no percentage, but this condition only enables Cancel above 0. As a result, install/update/uninstall always render the disabled spinner andonCancelis unreachable. Treat zero as an active, cancellable state.
plugins/plugin-catalog/src/components/plugins/Detail.tsx:411 - This loop captures the non-null
currentActionvalue from when polling starts.handleCancelsetting the state to null does not update that closure, so status requests continue after cancellation and may later report a spurious terminal error. Stop polling through an effect cleanup/cancellation ref rather than captured state.
This issue also appears on line 440 of the same file.
while (status && currentAction) {
plugins/plugin-catalog/src/components/plugins/Detail.tsx:454
- The detail request can reject on any proxy or ArtifactHub failure, but
initialize()does not catch it. The route then remains on the loader forever and emits an unhandled rejection. Handle this failure and show a recoverable error instead of leavingpluginDetailnull indefinitely.
const data = await fetchHeadlampPluginDetail(repoName, pluginName);
const enrichedPluginData = await checkIfPluginIsInstalled(data);
plugins/plugin-catalog/src/components/plugins/List.tsx:72
- The curated allowlist and default filtering are the core behavior of this feature, but no List unit test exercises matching by both normalized name and repository, rejecting near-matches, or switching between curated and full results. Add focused tests so edits to the JSON or filter logic cannot silently empty or broaden the optimized catalog.
This issue also appears on line 333 of the same file.
Replace the "only official/verified" filter with an "AKS Desktop optimized" filter driven by a curated allowlist shipped in src/aks-optimized-plugins.json. The full catalog is fetched and the allowlist is applied client-side, so curated plugins show regardless of their ArtifactHub official/verified status. Add resolveJsonModule and jsx to tsconfig.json to support importing the allowlist JSON. Signed-off-by: yolossn <sannagaraj@microsoft.com>
e14ac9b to
163f1f9
Compare
This was not part of the requirement, only the AKS optimsed toggle was discussed. |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 46 out of 49 changed files in this pull request and generated no new comments.
Suppressed comments (8)
plugins/plugin-catalog/src/components/plugins/List.tsx:382
- The pagination count is based on the unsearched catalog, while
pluginsis sliced fromfilteredPlugins. After a search narrows the results, users can still navigate to extra pages that are empty. Derive the count from the filtered collection.
plugins/plugin-catalog/src/components/plugins/List.tsx:331 - Loading a different catalog filter keeps the previous page index. For example, switching from page 2 of the full catalog to the three-item optimized catalog leaves
page === 2, slices to an empty list, and hides pagination because there is only one page. Reset the page when the fetched collection changes.
plugins/plugin-catalog/src/components/plugins/List.tsx:333 - A failed ArtifactHub/proxy request rejects
fetchAndProcessPlugins()without being caught.allPluginsthen remainsnull, so the catalog displays the loading indicator forever and the browser reports an unhandled rejection. Catch this failure and expose an error state with a retry or error message.
plugins/plugin-catalog/src/components/plugins/Detail.tsx:458 - When the same detail component receives new
repoName/pluginNameparameters, a non-empty selection from the previous plugin is preserved. The selector can therefore show no matching option and Install builds a URL for a version belonging to the previous plugin. Reset to the new plugin's latest version on route loads while preserving a choice only during action-triggered refreshes.
setSelectedVersion(prev => prev || enrichedPluginData.version);
plugins/plugin-catalog/src/components/plugins/Detail.tsx:455
- The mounted check occurs before both awaits, so cleanup cannot prevent a completed request from updating state. When detail route parameters change quickly, a slower response for the previous plugin can overwrite the new plugin's details. Re-check
isMountedafter the asynchronous work and before setting state.
const initialize = async () => {
if (isMounted) {
const data = await fetchHeadlampPluginDetail(repoName, pluginName);
const enrichedPluginData = await checkIfPluginIsInstalled(data);
setPluginDetail(enrichedPluginData);
plugins/plugin-catalog/src/components/plugins/Detail.tsx:462
- A failed detail request rejects
initialize()without any handler, leavingpluginDetailnull and the page on an indefinite loader. Handle this failure and render a retryable error state instead of producing an unhandled promise rejection.
initialize();
plugins/plugin-catalog/src/components/plugins/Detail.tsx:445
- If status polling throws, the action state is never cleared. The snackbar can be closed, but
currentActionremains set and the detail page stays on the loading/cancel control indefinitely. ClearcurrentActionalong with the other polling state in this failure path.
This issue also appears in the following locations of the same file:
- line 451
- line 458
- line 462
} catch (error) {
setCurrentActionState(null);
setCurrentActionMessage(null);
setCurrentActionProgress(0);
setAlertMessage(t('An unexpected error occurred: {{error}}', { error: String(error) }));
plugins/plugin-catalog/src/components/plugins/List.tsx:72
- The curated name/repository match is the core new behavior, but it has no unit coverage; the existing unit tests exercise only detail polling and proxy URL construction. Add a co-located
List.test.tsxcovering an allowlisted pair, name/repository mismatches, and optimized versus unfiltered behavior so allowlist edits cannot silently hide or admit the wrong plugins.
This issue also appears in the following locations of the same file:
- line 329
- line 333
- line 382
evangelos (skoeva)
left a comment
There was a problem hiding this comment.
awesome, thanks for this!
out of scope
Description
Bundles the Headlamp
plugin-catalogplugin into the AKS Desktop binary andreplaces its "only official/verified" filter with an "AKS Desktop optimized"
filter backed by a curated allowlist.
Type of Change
Related Issues
Closes #833
Related to #515
Changes Made
headlamp-k8s/plugins,
committed on its own so later changes are easy to review.
plugin-catalogto the build's plugin list so itis built and bundled with AKS Desktop.
"AKS Desktop optimized" toggle driven by
src/aks-optimized-plugins.json. Thefull ArtifactHub catalog is fetched and the allowlist is applied client-side,
so curated plugins appear regardless of their ArtifactHub official/verified
status. Editing the JSON changes what shows when the filter is on.
Notes
The curated allowlist currently contains flux, cert-manager, and keda; add
entries to
src/aks-optimized-plugins.jsonto expand it.