chore(managed-config): add configuration to disable local extensions (#16457)

### What does this PR do?

Adds a setting when set in
`~/.local/share/containers/podman-desktop/configuration/settings.json`
disables showing the "Local Extensions" tab.

### Screenshot / video of UI

`extensions.localExtensions.enabled": true`:

`extensions.localExtensions.enabled": false`:

### What issues does this PR fix or reference?

<!-- Include any related issues from Podman Desktop
repository (or from another issue tracker). -->

Closes https://github.com/podman-desktop/podman-desktop/issues/16037

### How to test this PR?

<!-- Please explain steps to verify the functionality,
do not forget to provide unit/component tests -->

- [X] Tests are covering the bug fix or the new feature

1. Edit `~/.local/share/containers/podman-desktop/configuration/settings.json`
2. Set `extensions.localExtensions.enabled": false`
3. `pnpm watch`
4. See there no more "Local Extensions" tab in Extensions section

Signed-off-by: Charlie Drage <charlie@charliedrage.com>
This commit is contained in:
Charlie Drage 2026-03-12 09:44:37 -04:00 committed by GitHub
parent a3d29f7b97
commit 8a6b9649a0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 121 additions and 45 deletions

View file

@ -163,6 +163,7 @@ const proxy: Proxy = {
const configurationRegistry: ConfigurationRegistry = {
getConfiguration: vi.fn(),
registerConfigurations: vi.fn(),
} as unknown as ConfigurationRegistry;
const originalConsoleError = console.error;
@ -398,3 +399,23 @@ test('Should use proxy object if proxySettings is undefined', () => {
// @ts-expect-error proxy property exists on https object
expect(options.agent?.https?.proxy.href).toBe('https://localhost/');
});
test('should register local extensions enabled configuration property', () => {
extensionsCatalog.init();
expect(configurationRegistry.registerConfigurations).toHaveBeenCalledWith([
expect.objectContaining({
id: 'preferences.extensions',
title: 'Extensions',
type: 'object',
properties: expect.objectContaining({
'extensions.localExtensions.enabled': {
description: 'Show the local extensions tab.',
type: 'boolean',
default: true,
hidden: true,
},
}),
}),
]);
});

View file

@ -69,6 +69,12 @@ export class ExtensionsCatalog {
default: ExtensionsCatalog.DEFAULT_EXTENSIONS_URL,
hidden: true,
},
['extensions.localExtensions.enabled']: {
description: 'Show the local extensions tab.',
type: 'boolean',
default: true,
hidden: true,
},
},
};

View file

@ -96,14 +96,14 @@ const combined: CombinedExtensionInfoUI[] = [
] as unknown[] as CombinedExtensionInfoUI[];
test('Expect to see extensions', async () => {
vi.mocked(window.getConfigurationValue).mockResolvedValue(true);
catalogExtensionInfos.set([aFakeExtension, bFakeExtension]);
extensionInfos.set(combined);
render(ExtensionList);
await vi.waitFor(() => {
const headingExtensions = screen.getByRole('heading', { name: 'extensions' });
expect(headingExtensions).toBeInTheDocument();
expect(screen.getByRole('heading', { name: 'extensions' })).toBeInTheDocument();
});
// get first extension
@ -124,77 +124,74 @@ test('Expect to see extensions', async () => {
});
test('Expect to see empty screen on extension page only', async () => {
vi.mocked(window.getConfigurationValue).mockResolvedValue(true);
catalogExtensionInfos.set([aFakeExtension]);
extensionInfos.set([]);
render(ExtensionList, { searchTerm: 'A' });
let title: HTMLElement | null;
await vi.waitFor(() => {
title = screen.queryByText(`No extensions matching 'A' found`);
expect(title).toBeInTheDocument();
expect(screen.queryByText(`No extensions matching 'A' found`)).toBeInTheDocument();
});
// click on the catalog
const catalogTab = screen.getByRole('button', { name: 'Catalog' });
await fireEvent.click(catalogTab);
title = screen.queryByText(`No extensions matching 'A' found`);
const title = screen.queryByText(`No extensions matching 'A' found`);
expect(title).not.toBeInTheDocument();
});
test('Expect to see empty screen on catalog page only', async () => {
vi.mocked(window.getConfigurationValue).mockResolvedValue(true);
catalogExtensionInfos.set([]);
extensionInfos.set(combined);
render(ExtensionList, { searchTerm: 'A' });
let title: HTMLElement | null;
await vi.waitFor(async () => {
title = screen.queryByText(`No extensions matching 'A' found`);
expect(title).not.toBeInTheDocument();
// click on the catalog
const catalogTab = screen.getByRole('button', { name: 'Catalog' });
await fireEvent.click(catalogTab);
await vi.waitFor(() => {
expect(screen.getByRole('button', { name: 'Catalog' })).toBeInTheDocument();
});
let title = screen.queryByText(`No extensions matching 'A' found`);
expect(title).not.toBeInTheDocument();
// click on the catalog
const catalogTab = screen.getByRole('button', { name: 'Catalog' });
await fireEvent.click(catalogTab);
title = screen.queryByText(`No extensions matching 'A' found`);
expect(title).toBeInTheDocument();
});
test('Expect to see empty screens on both pages', async () => {
vi.mocked(window.getConfigurationValue).mockResolvedValue(true);
catalogExtensionInfos.set([]);
extensionInfos.set([]);
render(ExtensionList, { searchTerm: 'foo' });
let title: HTMLElement | null;
await vi.waitFor(() => {
title = screen.getByText(`No extensions matching 'foo' found`);
expect(title).toBeInTheDocument();
expect(screen.getByText(`No extensions matching 'foo' found`)).toBeInTheDocument();
});
// click on the catalog
const catalogTab = screen.getByRole('button', { name: 'Catalog' });
await fireEvent.click(catalogTab);
title = screen.getByText(`No extensions matching 'foo' found`);
const title = screen.getByText(`No extensions matching 'foo' found`);
expect(title).toBeInTheDocument();
});
test('Search extension page searches also description', async () => {
vi.mocked(window.getConfigurationValue).mockResolvedValue(true);
catalogExtensionInfos.set([aFakeExtension]);
extensionInfos.set(combined);
render(ExtensionList, { searchTerm: 'bar' });
await vi.waitFor(() => {
const myExtension1 = screen.getByRole('region', { name: 'idAInstalled' });
expect(myExtension1).toBeInTheDocument();
expect(screen.getByRole('region', { name: 'idAInstalled' })).toBeInTheDocument();
});
// second extension should not be there as only in catalog (not installed) and doesn't have "bar" in the description
@ -204,25 +201,33 @@ test('Search extension page searches also description', async () => {
cleanup();
// Change the search
vi.mocked(window.getConfigurationValue).mockResolvedValue(true);
render(ExtensionList, { searchTerm: 'foo' });
await vi.waitFor(() => {
expect(window.getConfigurationValue).toHaveBeenCalled();
});
// The extension should not be there as it doesn't have "foo" in the description
const myExtension2 = screen.queryByRole('region', { name: 'idAInstalled' });
expect(myExtension2).not.toBeInTheDocument();
});
test('Search catalog page searches also description', async () => {
vi.mocked(window.getConfigurationValue).mockResolvedValue(true);
catalogExtensionInfos.set([aFakeExtension, bFakeExtension]);
extensionInfos.set([]);
render(ExtensionList, { searchTerm: 'bar' });
await vi.waitFor(async () => {
// Click on the catalog
const catalogTab = screen.getByRole('button', { name: 'Catalog' });
await fireEvent.click(catalogTab);
await vi.waitFor(() => {
expect(screen.getByRole('button', { name: 'Catalog' })).toBeInTheDocument();
});
// Click on the catalog
const catalogTab = screen.getByRole('button', { name: 'Catalog' });
await fireEvent.click(catalogTab);
// Verify that the extension containing "bar" in the description is displayed
const myExtension1 = screen.getByRole('group', { name: 'A Extension' });
expect(myExtension1).toBeInTheDocument();
@ -233,6 +238,10 @@ test('Search catalog page searches also description', async () => {
});
test('Expect to see local extensions tab content', async () => {
vi.mocked(window.getConfigurationValue).mockImplementation(async (key: string) => {
// Return true for local extensions enabled, false for development mode to show empty screen
return key === 'extensions.localExtensions.enabled';
});
catalogExtensionInfos.set([]);
extensionInfos.set([]);
@ -240,29 +249,34 @@ test('Expect to see local extensions tab content', async () => {
render(ExtensionList);
await vi.waitFor(async () => {
// select the local extensions tab
const localModeTab = screen.getByRole('button', { name: 'Local Extensions' });
await fireEvent.click(localModeTab);
await vi.waitFor(() => {
expect(screen.getByRole('button', { name: 'Local Extensions' })).toBeInTheDocument();
});
// select the local extensions tab
const localModeTab = screen.getByRole('button', { name: 'Local Extensions' });
await fireEvent.click(localModeTab);
// expect to see empty screen
const emptyText = screen.getByText('Enable Preferences > Extensions > Development Mode to test local extensions');
expect(emptyText).toBeInTheDocument();
});
test('Switching tabs keeps only terms in search term', async () => {
vi.mocked(window.getConfigurationValue).mockResolvedValue(true);
catalogExtensionInfos.set([aFakeExtension, bFakeExtension]);
extensionInfos.set([]);
render(ExtensionList, { searchTerm: 'bar category:bar not:installed' });
await vi.waitFor(async () => {
// Click on the catalog
const catalogTab = screen.getByRole('button', { name: 'Catalog' });
await fireEvent.click(catalogTab);
await vi.waitFor(() => {
expect(screen.getByRole('button', { name: 'Catalog' })).toBeInTheDocument();
});
// Click on the catalog
const catalogTab = screen.getByRole('button', { name: 'Catalog' });
await fireEvent.click(catalogTab);
// Verify that the extension containing "bar" in the description is displayed (which is not in bar category and is installed)
// meaning that `category:bar not:installed` has been removed from search term
const myExtension1 = screen.getByRole('group', { name: 'A Extension' });
@ -273,8 +287,7 @@ test('Expect install custom button is visible', async () => {
render(ExtensionList);
await vi.waitFor(() => {
const installCustomButton = screen.getByRole('button', { name: 'Install custom' });
expect(installCustomButton).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Install custom' })).toBeInTheDocument();
});
});
@ -284,7 +297,36 @@ test('Expect install custom button to not be visible if extensions.customExtensi
render(ExtensionList);
await vi.waitFor(() => {
const installCustomButton = screen.queryByRole('button', { name: 'Install custom' });
expect(installCustomButton).not.toBeInTheDocument();
expect(window.getConfigurationValue).toHaveBeenCalled();
});
const installCustomButton = screen.queryByRole('button', { name: 'Install custom' });
expect(installCustomButton).not.toBeInTheDocument();
});
test('Expect local extensions tab is visible', async () => {
vi.mocked(window.getConfigurationValue).mockResolvedValue(undefined);
catalogExtensionInfos.set([]);
extensionInfos.set([]);
render(ExtensionList);
await vi.waitFor(() => {
expect(screen.getByRole('button', { name: 'Local Extensions' })).toBeInTheDocument();
});
});
test('Expect local extensions tab to not be visible if extensions.localExtensions.enabled is false', async () => {
vi.mocked(window.getConfigurationValue).mockResolvedValue(false);
catalogExtensionInfos.set([]);
extensionInfos.set([]);
render(ExtensionList);
await vi.waitFor(() => {
expect(window.getConfigurationValue).toHaveBeenCalled();
});
const localExtensionsTab = screen.queryByRole('button', { name: 'Local Extensions' });
expect(localExtensionsTab).not.toBeInTheDocument();
});

View file

@ -28,6 +28,10 @@ let enableCustomExtensions = $derived(
(await window.getConfigurationValue('extensions.customExtensions.enabled')) ?? true,
);
let enableLocalExtensions = $derived(
(await window.getConfigurationValue('extensions.localExtensions.enabled')) ?? true,
);
const filteredInstalledExtensions: CombinedExtensionInfoUI[] = $derived(
extensionsUtils.filterInstalledExtensions($combinedInstalledExtensions, searchTerm),
);
@ -104,12 +108,14 @@ function changeScreen(newScreen: 'installed' | 'catalog' | 'development'): void
changeScreen('catalog');
}}
selected={screen === 'catalog'}>Catalog</Button>
{#if enableLocalExtensions}
<Button
type="tab"
on:click={(): void => {
changeScreen('development');
}}
selected={screen === 'development'}>Local Extensions</Button>
type="tab"
on:click={(): void => {
changeScreen('development');
}}
selected={screen === 'development'}>Local Extensions</Button>
{/if}
{/snippet}
{#snippet content()}
@ -132,7 +138,7 @@ function changeScreen(newScreen: 'installed' | 'catalog' | 'development'): void
on:resetFilter={(): string => (searchTerm = '')} />
{/if}
<CatalogExtensionList showEmptyScreen={!searchTerm} catalogExtensions={filteredCatalogExtensions} />
{:else if screen === 'development'}
{:else if screen === 'development' && enableLocalExtensions}
<DevelopmentExtensionList />
{/if}
</div>

View file

@ -52,6 +52,7 @@ import TabItem from '@theme/TabItem';
| `extensions.customExtensions.enabled` | boolean | `true` | When disabled, the `Install custom...` button on the Extensions page will not appear. |
| `extensions.ignoreBannerRecommendations` | boolean | `false` | Disable recommendation banners |
| `extensions.ignoreRecommendations` | boolean | `false` | Disable extension recommendations |
| `extensions.localExtensions.enabled` | boolean | `true` | Show the local extensions tab |
| `feedback.dialog` | boolean | `true` | Show experimental feature feedback dialog |
| `kubernetes.Kubeconfig` | string | `"~/.kube/config"` | Path to kubeconfig file |
| `kubernetes.statesExperimental` | object | `null` | **EXPERIMENTAL:** New context monitoring. Example: `{}` |