mirror of
https://github.com/podman-desktop/podman-desktop
synced 2026-04-21 17:47:22 +00:00
chore(config): add setting to disable extension catalog (#16425)
### What does this PR do? Adds a setting to disable the extension catalog throughout PD. This can be done by adding the setting manually to your `~/.local/share/containers/podman-desktop/configuration/settings.json` with: ``` "extensions.catalog.enabled": false ``` Which will disable the appearance of the catalog throughout PD. ### Screenshot / video of UI <!-- If this PR is changing UI, please include screenshots or screencasts showing the difference --> ### 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/16036 ### 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. Add: `"extensions.catalog.enabled": false` 3. Start / Restart PD 4. See that you cannot access the catalog from the following places: Extensions, Settings > Authentication, and Kubernetes (with no context). Signed-off-by: Charlie Drage <charlie@charliedrage.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
07831c04cc
commit
b6a6aac90a
9 changed files with 116 additions and 25 deletions
|
|
@ -19,4 +19,5 @@
|
|||
export enum ExtensionsCatalogSettings {
|
||||
SectionName = 'extensions',
|
||||
registryUrl = 'registryUrl',
|
||||
catalogEnabled = 'catalog.enabled',
|
||||
}
|
||||
|
|
|
|||
|
|
@ -400,7 +400,7 @@ test('Should use proxy object if proxySettings is undefined', () => {
|
|||
expect(options.agent?.https?.proxy.href).toBe('https://localhost/');
|
||||
});
|
||||
|
||||
test('should register local extensions enabled configuration property', () => {
|
||||
test('should register local extensions and catalog enabled configuration properties', () => {
|
||||
extensionsCatalog.init();
|
||||
|
||||
expect(configurationRegistry.registerConfigurations).toHaveBeenCalledWith([
|
||||
|
|
@ -415,6 +415,12 @@ test('should register local extensions enabled configuration property', () => {
|
|||
default: true,
|
||||
hidden: true,
|
||||
},
|
||||
'extensions.catalog.enabled': {
|
||||
description: 'Show the extension catalog in the UI. When disabled, hides the catalog suggestions.',
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
hidden: true,
|
||||
},
|
||||
}),
|
||||
}),
|
||||
]);
|
||||
|
|
|
|||
|
|
@ -75,6 +75,12 @@ export class ExtensionsCatalog {
|
|||
default: true,
|
||||
hidden: true,
|
||||
},
|
||||
[ExtensionsCatalogSettings.SectionName + '.' + ExtensionsCatalogSettings.catalogEnabled]: {
|
||||
description: 'Show the extension catalog in the UI. When disabled, hides the catalog suggestions.',
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
hidden: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -34,6 +34,8 @@ beforeAll(() => {
|
|||
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks();
|
||||
// default to catalog enabled
|
||||
vi.mocked(window.getConfigurationValue).mockResolvedValue(true);
|
||||
});
|
||||
|
||||
const aFakeExtension: CatalogExtension = {
|
||||
|
|
@ -218,3 +220,29 @@ test('empty catalog, hide if empty', async () => {
|
|||
const emptyMsg = screen.queryByText('No extensions in the catalog');
|
||||
expect(emptyMsg).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('render nothing when catalog is disabled', async () => {
|
||||
vi.mocked(window.getConfigurationValue).mockResolvedValue(false);
|
||||
catalogExtensionInfos.set([aFakeExtension, bFakeExtension]);
|
||||
extensionInfos.set(combined);
|
||||
|
||||
render(EmbeddableCatalogExtensionList, {});
|
||||
|
||||
await vi.waitFor(() => {
|
||||
expect(screen.queryByText('Available extensions')).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole('group', { name: 'A Extension' })).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole('group', { name: 'B Extension' })).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
test('render extensions when catalog is enabled', async () => {
|
||||
vi.mocked(window.getConfigurationValue).mockResolvedValue(true);
|
||||
catalogExtensionInfos.set([aFakeExtension, bFakeExtension]);
|
||||
extensionInfos.set(combined);
|
||||
|
||||
render(EmbeddableCatalogExtensionList, {});
|
||||
|
||||
await vi.waitFor(() => {
|
||||
expect(screen.queryByText('Available extensions')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { derived, type Readable } from 'svelte/store';
|
||||
|
||||
import { combinedInstalledExtensions } from '/@/stores/all-installed-extensions';
|
||||
|
|
@ -20,6 +21,13 @@ export let ondetails: (extensionId: string) => void = () => {};
|
|||
// show installed extensions
|
||||
export let showInstalled: boolean = true;
|
||||
|
||||
let enableCatalog = true;
|
||||
|
||||
onMount(async () => {
|
||||
const value = await window.getConfigurationValue<boolean>('extensions.catalog.enabled');
|
||||
enableCatalog = value ?? true;
|
||||
});
|
||||
|
||||
const extensionsUtils = new ExtensionsUtils();
|
||||
|
||||
const catalogExtensions: Readable<CatalogExtensionInfoUI[]> = derived(
|
||||
|
|
@ -53,6 +61,7 @@ const catalogExtensions: Readable<CatalogExtensionInfoUI[]> = derived(
|
|||
);
|
||||
</script>
|
||||
|
||||
{#if enableCatalog}
|
||||
<div class="flex bg-[var(--pd-content-bg)] text-left">
|
||||
<CatalogExtensionList
|
||||
oninstall={oninstall}
|
||||
|
|
@ -61,3 +70,4 @@ const catalogExtensions: Readable<CatalogExtensionInfoUI[]> = derived(
|
|||
showEmptyScreen={showEmptyScreen}
|
||||
catalogExtensions={$catalogExtensions} />
|
||||
</div>
|
||||
{/if}
|
||||
|
|
|
|||
|
|
@ -239,8 +239,8 @@ 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';
|
||||
// Return true for local extensions and catalog enabled
|
||||
return key === 'extensions.localExtensions.enabled' || key === 'extensions.catalog.enabled';
|
||||
});
|
||||
catalogExtensionInfos.set([]);
|
||||
extensionInfos.set([]);
|
||||
|
|
@ -330,3 +330,28 @@ test('Expect local extensions tab to not be visible if extensions.localExtension
|
|||
const localExtensionsTab = screen.queryByRole('button', { name: 'Local Extensions' });
|
||||
expect(localExtensionsTab).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('Expect catalog tab is visible', async () => {
|
||||
vi.mocked(window.getConfigurationValue).mockResolvedValue(undefined);
|
||||
catalogExtensionInfos.set([]);
|
||||
extensionInfos.set([]);
|
||||
|
||||
render(ExtensionList);
|
||||
|
||||
await vi.waitFor(() => {
|
||||
const catalogTab = screen.getByRole('button', { name: 'Catalog' });
|
||||
expect(catalogTab).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
test('Expect catalog tab to not be visible if extensions.catalog.enabled is false', async () => {
|
||||
vi.mocked(window.getConfigurationValue).mockResolvedValue(false);
|
||||
catalogExtensionInfos.set([]);
|
||||
extensionInfos.set([]);
|
||||
|
||||
render(ExtensionList);
|
||||
|
||||
await vi.waitFor(() => {
|
||||
expect(screen.queryByRole('button', { name: 'Catalog' })).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -32,6 +32,8 @@ let enableLocalExtensions = $derived(
|
|||
(await window.getConfigurationValue('extensions.localExtensions.enabled')) ?? true,
|
||||
);
|
||||
|
||||
let enableCatalog = $derived((await window.getConfigurationValue('extensions.catalog.enabled')) ?? true);
|
||||
|
||||
const filteredInstalledExtensions: CombinedExtensionInfoUI[] = $derived(
|
||||
extensionsUtils.filterInstalledExtensions($combinedInstalledExtensions, searchTerm),
|
||||
);
|
||||
|
|
@ -102,12 +104,14 @@ function changeScreen(newScreen: 'installed' | 'catalog' | 'development'): void
|
|||
changeScreen('installed');
|
||||
}}
|
||||
selected={screen === 'installed'}>Installed</Button>
|
||||
<Button
|
||||
type="tab"
|
||||
on:click={(): void => {
|
||||
changeScreen('catalog');
|
||||
}}
|
||||
selected={screen === 'catalog'}>Catalog</Button>
|
||||
{#if enableCatalog}
|
||||
<Button
|
||||
type="tab"
|
||||
on:click={(): void => {
|
||||
changeScreen('catalog');
|
||||
}}
|
||||
selected={screen === 'catalog'}>Catalog</Button>
|
||||
{/if}
|
||||
{#if enableLocalExtensions}
|
||||
<Button
|
||||
type="tab"
|
||||
|
|
@ -129,7 +133,7 @@ function changeScreen(newScreen: 'installed' | 'catalog' | 'development'): void
|
|||
on:resetFilter={(): string => (searchTerm = '')} />
|
||||
{/if}
|
||||
<InstalledExtensionList extensionInfos={filteredInstalledExtensions} />
|
||||
{:else if screen === 'catalog'}
|
||||
{:else if screen === 'catalog' && enableCatalog}
|
||||
{#if searchTerm && filteredCatalogExtensions.length === 0}
|
||||
<FilteredEmptyScreen
|
||||
icon={ExtensionIcon}
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@
|
|||
import '@testing-library/jest-dom/vitest';
|
||||
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/svelte';
|
||||
import { afterEach, beforeAll, expect, test, vi } from 'vitest';
|
||||
import { afterEach, beforeAll, beforeEach, expect, test, vi } from 'vitest';
|
||||
|
||||
import { AppearanceUtil } from '/@/lib/appearance/appearance-util';
|
||||
import { authenticationProviders } from '/@/stores/authenticationProviders';
|
||||
|
|
@ -44,17 +44,22 @@ beforeAll(() => {
|
|||
(window as any).getConfigurationValue = configMock;
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
// ensure we mock the config to not block rendering of the component (individual tests can override)
|
||||
configMock.mockResolvedValue(undefined);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
test('Expect that page shows icon and message when no auth providers registered', () => {
|
||||
test('Expect that page shows icon and message when no auth providers registered', async () => {
|
||||
render(PreferencesAuthenticationProvidersRendering, {});
|
||||
const noProvidersText = screen.getByText('No authentication providers');
|
||||
const noProvidersText = await waitFor(() => screen.getByText('No authentication providers'));
|
||||
expect(noProvidersText).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('Expect that page shows registered authentication providers without accounts as logged out', () => {
|
||||
test('Expect that page shows registered authentication providers without accounts as logged out', async () => {
|
||||
authenticationProviders.set([
|
||||
{
|
||||
id: 'test',
|
||||
|
|
@ -63,7 +68,7 @@ test('Expect that page shows registered authentication providers without account
|
|||
},
|
||||
]);
|
||||
render(PreferencesAuthenticationProvidersRendering, {});
|
||||
const listOfProviders = screen.getByRole('list');
|
||||
const listOfProviders = await waitFor(() => screen.getByRole('list'));
|
||||
expect(listOfProviders).toBeInTheDocument();
|
||||
const providerItem = screen.getByRole('listitem', { name: 'Test Authentication Provider' });
|
||||
expect(providerItem).toBeInTheDocument();
|
||||
|
|
@ -89,10 +94,10 @@ const testProvidersInfo = [
|
|||
},
|
||||
];
|
||||
|
||||
test('Expect that page shows registered authentication providers with account as logged in', () => {
|
||||
test('Expect that page shows registered authentication providers with account as logged in', async () => {
|
||||
authenticationProviders.set(testProvidersInfo);
|
||||
render(PreferencesAuthenticationProvidersRendering, {});
|
||||
const providerName = screen.getByLabelText('Provider Name');
|
||||
const providerName = await waitFor(() => screen.getByLabelText('Provider Name'));
|
||||
expect(providerName).toHaveTextContent('Test Authentication Provider');
|
||||
const providerStatus = screen.getByLabelText('Provider Status');
|
||||
expect(providerStatus).toBeInTheDocument();
|
||||
|
|
@ -107,7 +112,9 @@ test('Expect that page shows registered authentication providers with account as
|
|||
test('Expect Sign Out button click calls window.requestAuthenticationProviderSignOut with provider and account ids', async () => {
|
||||
authenticationProviders.set(testProvidersInfo);
|
||||
render(PreferencesAuthenticationProvidersRendering, {});
|
||||
const signoutButton = screen.getByRole('button', { name: `Sign out of ${testProvidersInfo[0].accounts[0].label}` });
|
||||
const signoutButton = await waitFor(() =>
|
||||
screen.getByRole('button', { name: `Sign out of ${testProvidersInfo[0].accounts[0].label}` }),
|
||||
);
|
||||
const requestSignOutMock = vi.fn().mockImplementation(() => {});
|
||||
(window as any).requestAuthenticationProviderSignOut = requestSignOutMock;
|
||||
await fireEvent.click(signoutButton);
|
||||
|
|
@ -126,8 +133,9 @@ const testProvidersInfoWithoutSessionRequests = [
|
|||
test('Expect Sign in button to be hidden when there are no session requests', async () => {
|
||||
authenticationProviders.set(testProvidersInfoWithoutSessionRequests);
|
||||
render(PreferencesAuthenticationProvidersRendering, {});
|
||||
const menuButton = screen.queryAllByRole('button', { name: 'Sign in' });
|
||||
expect(menuButton.length).equals(0); // no menu button
|
||||
await waitFor(() => {
|
||||
expect(screen.queryAllByRole('button', { name: 'Sign in' }).length).equals(0);
|
||||
});
|
||||
});
|
||||
|
||||
const testProvidersInfoWithSessionRequests = [
|
||||
|
|
@ -152,7 +160,7 @@ test('Expect Sign In button to be visible when there is only one session request
|
|||
const requestSignInMock = vi.fn();
|
||||
(window as any).requestAuthenticationProviderSignIn = requestSignInMock;
|
||||
render(PreferencesAuthenticationProvidersRendering, {});
|
||||
const menuButton = screen.getByRole('button', { name: 'Sign in' });
|
||||
const menuButton = await waitFor(() => screen.getByRole('button', { name: 'Sign in' }));
|
||||
|
||||
const tooltipTrigger = screen.getByTestId('tooltip-trigger');
|
||||
await fireEvent.mouseEnter(tooltipTrigger);
|
||||
|
|
@ -191,7 +199,7 @@ test('Expect Sign In popup menu to be visible when there is more than one sessio
|
|||
authenticationProviders.set(testProvidersInfoWithMultipleSessionRequests);
|
||||
(window as any).requestAuthenticationProviderSignIn = vi.fn();
|
||||
render(PreferencesAuthenticationProvidersRendering, {});
|
||||
const menuButton = screen.getByRole('button', { name: 'kebab menu' });
|
||||
const menuButton = await waitFor(() => screen.getByRole('button', { name: 'kebab menu' }));
|
||||
await fireEvent.click(menuButton);
|
||||
// test sign in with extension1
|
||||
const menuItem1 = screen.getByText('Sign in to use Extension1 Label');
|
||||
|
|
@ -210,9 +218,11 @@ test('Expect Sign In popup menu to be visible when there is more than one sessio
|
|||
test('Expects default icon to be used when provider has no images option', async () => {
|
||||
authenticationProviders.set(testProvidersInfoWithSessionRequests);
|
||||
render(PreferencesAuthenticationProvidersRendering, {});
|
||||
screen.getByRole('img', {
|
||||
name: `Default icon for ${testProvidersInfoWithSessionRequests[0].displayName} provider`,
|
||||
});
|
||||
await waitFor(() =>
|
||||
screen.getByRole('img', {
|
||||
name: `Default icon for ${testProvidersInfoWithSessionRequests[0].displayName} provider`,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
test('Expects images.icon option to be used when no themes are present', async () => {
|
||||
|
|
|
|||
|
|
@ -49,6 +49,7 @@ import TabItem from '@theme/TabItem';
|
|||
| `editor.integrated.fontSize` | number | `10` | Editor font size (6-100 px) |
|
||||
| `extensions.autoCheckUpdates` | boolean | `true` | Auto-check for extension updates |
|
||||
| `extensions.autoUpdate` | boolean | `true` | Auto-install extension updates |
|
||||
| `extensions.catalog.enabled` | boolean | `true` | Show the extension catalog in the UI |
|
||||
| `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 |
|
||||
|
|
|
|||
Loading…
Reference in a new issue