Skip to content

plugins: Ship plugin-catalog with an AKS Desktop optimized filter - #848

Merged
evangelos (skoeva) merged 3 commits into
mainfrom
ship-plugin-catalog
Aug 13, 2026
Merged

plugins: Ship plugin-catalog with an AKS Desktop optimized filter#848
evangelos (skoeva) merged 3 commits into
mainfrom
ship-plugin-catalog

Conversation

@yolossn

Copy link
Copy Markdown
Member

Description

Bundles the Headlamp plugin-catalog plugin into the AKS Desktop binary and
replaces its "only official/verified" filter with an "AKS Desktop optimized"
filter backed by a curated allowlist.

image

Type of Change

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • Documentation update
  • Performance improvement
  • Code refactoring
  • CI/CD changes
  • Other: **___**

Related Issues

Closes #833
Related to #515

Changes Made

  • Vendor plugin-catalog - copy of the plugin from
    headlamp-k8s/plugins,
    committed on its own so later changes are easy to review.
  • Build integration — add plugin-catalog to the build's plugin list so it
    is built and bundled with AKS Desktop.
  • AKS Desktop optimized filter — swap the official/verified toggle for an
    "AKS Desktop optimized" toggle driven by src/aks-optimized-plugins.json. The
    full 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.json to expand it.

Copilot AI lite review requested due to automatic review settings August 6, 2026 05:30
Comment thread plugins/plugin-catalog/src/components/plugins/Detail.tsx Dismissed

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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-catalog plugin with catalog, detail, and installed-plugins views.
  • Introduce an “AKS Desktop optimized” filter driven by src/aks-optimized-plugins.json and persisted via plugin settings.
  • Bundle plugin-catalog during packaging by adding it to build/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

  • Loading story doesn’t set required args, which makes the rendered output differ from the stored storyshot snapshot (e.g. search becomes undefined, 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.

Comment thread plugins/plugin-catalog/src/components/plugins/InstalledList.tsx Outdated
Comment thread plugins/plugin-catalog/src/components/plugins/List.tsx
Comment thread plugins/plugin-catalog/locales/en/translation.json
Comment thread plugins/plugin-catalog/src/components/plugins/List.tsx Outdated
Comment thread plugins/plugin-catalog/src/components/plugins/Detail.tsx Outdated
Comment thread plugins/plugin-catalog/src/components/plugins/List.tsx
Comment thread plugins/plugin-catalog/src/components/plugins/Detail.tsx
@illume
René Dudfield (illume) requested a balanced review from Copilot August 6, 2026 18:08

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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 currentAction triggers this initialization effect, which calls fetchStatus() 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

  • folderName is a plugin folder/package identifier, and this component passes it as the pluginName route segment at line 73. Typing it as a boolean permits invalid detail links and contradicts the PluginManager.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 installedPlugins prop, while PurePluginInstalledList requires catalogPlugins and nonCatalogPlugins. 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 installedPlugins prop and omits the current list props, so its args no longer match PurePluginInstalledListProps.
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 error prevents 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; true would generate an invalid plugin detail URL once this fixture is passed through catalogPlugins.
    folderName: true,

plugins/plugin-catalog/src/components/plugins/List.tsx:337

  • If any ArtifactHub/proxy request rejects, this effect leaves allPlugins as null, 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
  • totalPages was computed before applying search, so a narrow search still exposes pages from the full catalog; selecting one of them produces an empty result page. Derive the count from filteredPlugins while 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/pluginName changes, 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 currentAction is 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-catalog is absent from both the localization source registry (Localize/translation-manager.mjs:22-37) and CI's TRANSLATION_PLUGINS list (.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/plugins checkout 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: folderName is 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,

Comment thread plugins/plugin-catalog/src/components/plugins/List.tsx Outdated

@illume René Dudfield (illume) left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Please check the copilot comments after you push?

What happens to the existing CNCF / Official switch and the other third selection?

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>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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 getStatus throws, this catch clears the status fields but leaves currentAction non-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. allPlugins remains 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 installedPlugins prop and omits both required list props, so the co-located npm run tsc check 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 setting error means 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

  • PurePluginInstalledList has no installedPlugins prop; it expects catalogPlugins and nonCatalogPlugins. 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 from filteredPlugins so 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 currentActionProgress at 0 because PluginManager exposes no percentage, but this condition only enables Cancel above 0. As a result, install/update/uninstall always render the disabled spinner and onCancel is unreachable. Treat zero as an active, cancellable state.
    plugins/plugin-catalog/src/components/plugins/Detail.tsx:411
  • This loop captures the non-null currentAction value from when polling starts. handleCancel setting 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 leaving pluginDetail null 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>
@yolossn
S Santhosh Nagaraj (yolossn) marked this pull request as ready for review August 13, 2026 16:53
Copilot AI review requested due to automatic review settings August 13, 2026 16:53
@yolossn

Copy link
Copy Markdown
Member Author

What happens to the existing CNCF / Official switch and the other third selection?

This was not part of the requirement, only the AKS optimsed toggle was discussed.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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 plugins is sliced from filteredPlugins. 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. allPlugins then remains null, 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/pluginName parameters, 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 isMounted after 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, leaving pluginDetail null 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 currentAction remains set and the detail page stays on the loading/cancel control indefinitely. Clear currentAction along 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.tsx covering 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

@skoeva evangelos (skoeva) left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

awesome, thanks for this!

@skoeva
evangelos (skoeva) merged commit c7ef9ad into main Aug 13, 2026
25 checks passed
@skoeva
evangelos (skoeva) deleted the ship-plugin-catalog branch August 13, 2026 17:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[FEATURE] Enable Plugins: Add filter between aks optimized / headlamp official plugins

4 participants