Skip to content

Commit 0fcc5ff

Browse files
authored
feat: navigate to catalog (podman-desktop#14656)
feat: navigate to extensions catalog Signed-off-by: Philippe Martin <phmartin@redhat.com>
1 parent 490fb69 commit 0fcc5ff

File tree

12 files changed

+143
-5
lines changed

12 files changed

+143
-5
lines changed

packages/api/src/navigation-page.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,4 +51,5 @@ export enum NavigationPage {
5151
EXPERIMENTAL_FEATURES = 'experimental',
5252
CREATE_PROVIDER_CONNECTION = 'create-provider-connection',
5353
NETWORK = 'network',
54+
EXTENSIONS_CATALOG = 'extensions-catalog',
5455
}

packages/api/src/navigation-request.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ export interface NavigationParameters {
5454
[NavigationPage.PROVIDER_TASK]: { internalId: string; taskId: number | undefined };
5555
[NavigationPage.EXPERIMENTAL_FEATURES]: never;
5656
[NavigationPage.NETWORK]: { name: string; engineId: string };
57+
[NavigationPage.EXTENSIONS_CATALOG]: { searchTerm?: string };
5758
}
5859

5960
// the parameters property is optional when the NavigationParameters say it is

packages/extension-api/src/extension-api.d.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4939,6 +4939,13 @@ declare module '@podman-desktop/api' {
49394939
export function getFreePort(port: number): Promise<number>;
49404940
}
49414941

4942+
export interface NavigateToExtensionsCatalogOptions {
4943+
/**
4944+
* the search term to use to filter the extensions. Supports `category:`, `keyword:` prefixes and `is:installed`, `not:installed`.
4945+
*/
4946+
readonly searchTerm?: string;
4947+
}
4948+
49424949
export namespace navigation {
49434950
// Navigate to the Dashboard page
49444951
export function navigateToDashboard(): Promise<void>;
@@ -5004,6 +5011,11 @@ declare module '@podman-desktop/api' {
50045011
*/
50055012
export function navigateToOnboarding(extensionId?: string): Promise<void>;
50065013

5014+
/**
5015+
* Navigate to the Catalog tab of the Extensions page
5016+
*/
5017+
export function navigateToExtensionsCatalog(options: NavigateToExtensionsCatalogOptions): Promise<void>;
5018+
50075019
/**
50085020
* Allow to define custom route for an extension.
50095021
*

packages/main/src/plugin/extension/extension-loader.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1546,6 +1546,11 @@ export class ExtensionLoader implements IAsyncDisposable {
15461546
onboardingExtensionId ??= extensionInfo.id;
15471547
await this.navigationManager.navigateToOnboarding(onboardingExtensionId);
15481548
},
1549+
navigateToExtensionsCatalog: async (
1550+
options: containerDesktopAPI.NavigateToExtensionsCatalogOptions,
1551+
): Promise<void> => {
1552+
await this.navigationManager.navigateToExtensionsCatalog(options);
1553+
},
15491554
navigate: async (routeId: string, ...args: unknown[]): Promise<void> => {
15501555
return this.navigationManager.navigateToRoute(`${extensionInfo.id}.${routeId}`, args);
15511556
},

packages/main/src/plugin/navigation/navigation-manager.spec.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -293,3 +293,14 @@ test('check navigateToCreateProviderConnection', async () => {
293293
},
294294
});
295295
});
296+
297+
test('check navigateToExtensionsCatalog', async () => {
298+
await navigationManager.navigateToExtensionsCatalog({ searchTerm: 'not:installed category:foo keyword:bar' });
299+
300+
expect(apiSender.send).toHaveBeenCalledWith('navigate', {
301+
page: NavigationPage.EXTENSIONS_CATALOG,
302+
parameters: {
303+
searchTerm: 'not:installed category:foo keyword:bar',
304+
},
305+
});
306+
});

packages/main/src/plugin/navigation/navigation-manager.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
* SPDX-License-Identifier: Apache-2.0
1717
***********************************************************************/
1818

19-
import type { ProviderContainerConnection } from '@podman-desktop/api';
19+
import type { NavigateToExtensionsCatalogOptions, ProviderContainerConnection } from '@podman-desktop/api';
2020
import { inject, injectable } from 'inversify';
2121

2222
import { ApiSenderType } from '/@/plugin/api.js';
@@ -342,4 +342,13 @@ export class NavigationManager {
342342
},
343343
});
344344
}
345+
346+
async navigateToExtensionsCatalog(options: NavigateToExtensionsCatalogOptions): Promise<void> {
347+
this.navigateTo({
348+
page: NavigationPage.EXTENSIONS_CATALOG,
349+
parameters: {
350+
searchTerm: options.searchTerm,
351+
},
352+
});
353+
}
345354
}

packages/renderer/src/App.svelte

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import '@fortawesome/fontawesome-free/css/all.min.css';
55
import { tablePersistence } from '@podman-desktop/ui-svelte';
66
import { router } from 'tinro';
77
8+
import { parseExtensionListRequest } from '/@/lib/extensions/extension-list';
89
import PinActions from '/@/lib/statusbar/PinActions.svelte';
910
import { handleNavigation } from '/@/navigation';
1011
import { kubernetesNoCurrentContext } from '/@/stores/kubernetes-no-current-context';
@@ -395,8 +396,12 @@ tablePersistence.storage = new PodmanDesktopStoragePersist();
395396
<Route path="/troubleshooting/*" breadcrumb="Troubleshooting">
396397
<TroubleshootingPage />
397398
</Route>
398-
<Route path="/extensions" breadcrumb="Extensions" navigationHint="root">
399-
<ExtensionList />
399+
<Route path="/extensions" breadcrumb="Extensions" navigationHint="root" let:meta>
400+
{@const request = parseExtensionListRequest(meta)}
401+
<ExtensionList
402+
searchTerm={request.searchTerm}
403+
screen={request.screen}
404+
/>
400405
</Route>
401406
<Route path="/extensions/details/:id/*" breadcrumb="Extension Details" let:meta navigationHint="details">
402407
<ExtensionDetails extensionId={meta.params.id} />

packages/renderer/src/lib/extensions/ExtensionList.svelte

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import { faCloudDownload } from '@fortawesome/free-solid-svg-icons';
33
import { Button, FilteredEmptyScreen, NavPage } from '@podman-desktop/ui-svelte';
44
5+
import type { ExtensionListScreen } from '/@/lib/extensions/extension-list';
56
import InstalledExtensionList from '/@/lib/extensions/InstalledExtensionList.svelte';
67
import ExtensionIcon from '/@/lib/images/ExtensionIcon.svelte';
78
import { type CombinedExtensionInfoUI, combinedInstalledExtensions } from '/@/stores/all-installed-extensions';
@@ -16,9 +17,10 @@ import InstallManuallyExtensionModal from './InstallManuallyExtensionModal.svelt
1617
1718
interface Props {
1819
searchTerm?: string;
20+
screen?: ExtensionListScreen;
1921
}
2022
21-
let { searchTerm = '' }: Props = $props();
23+
let { searchTerm = '', screen = 'installed' }: Props = $props();
2224
2325
const extensionsUtils = new ExtensionsUtils();
2426
@@ -48,7 +50,6 @@ function closeModal(): void {
4850
installManualImageModal = false;
4951
}
5052
51-
let screen: 'installed' | 'catalog' | 'development' = $state('installed');
5253
let installManualImageModal: boolean = $state(false);
5354
5455
function changeScreen(newScreen: 'installed' | 'catalog' | 'development'): void {
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/**********************************************************************
2+
* Copyright (C) 2025 Red Hat, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*
16+
* SPDX-License-Identifier: Apache-2.0
17+
***********************************************************************/
18+
19+
import { describe, expect, it } from 'vitest';
20+
21+
import { parseExtensionListRequest } from './extension-list';
22+
23+
describe('parseExtensionListRequest', () => {
24+
it('should return the correct query params when specified correctly', () => {
25+
const request = { query: { searchTerm: 'category%3Akubernetes%20keyword%3Aprovider%20term', screen: 'catalog' } };
26+
const result = parseExtensionListRequest(request);
27+
expect(result).toEqual({ searchTerm: 'category:kubernetes keyword:provider term', screen: 'catalog' });
28+
});
29+
30+
it('should return the correct query params when screen is not specified correctly', () => {
31+
const request = { query: { searchTerm: 'test', screen: 'unknown' } };
32+
const result = parseExtensionListRequest(request);
33+
expect(result).toEqual({ searchTerm: 'test', screen: 'installed' });
34+
});
35+
36+
it('should return the correct query params when nothing is specified', () => {
37+
const request = {};
38+
const result = parseExtensionListRequest(request);
39+
expect(result).toEqual({ searchTerm: '', screen: 'installed' });
40+
});
41+
});
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
/**********************************************************************
2+
* Copyright (C) 2025 Red Hat, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*
16+
* SPDX-License-Identifier: Apache-2.0
17+
***********************************************************************/
18+
19+
const screens = ['installed', 'catalog', 'development'] as const;
20+
21+
export type ExtensionListScreen = (typeof screens)[number];
22+
23+
interface ExtensionListRequest {
24+
searchTerm: string;
25+
screen: ExtensionListScreen;
26+
}
27+
28+
export function parseExtensionListRequest(request: { query?: Record<string, string> }): ExtensionListRequest {
29+
return {
30+
searchTerm: request.query?.searchTerm ? decodeURIComponent(request.query.searchTerm) : '',
31+
screen: screens.includes(request.query?.screen as ExtensionListScreen)
32+
? (request.query?.screen as ExtensionListScreen)
33+
: 'installed',
34+
};
35+
}

0 commit comments

Comments
 (0)