mirror of
https://github.com/podman-desktop/podman-desktop
synced 2026-04-21 09:37:22 +00:00
feat: added entries from navigation to searchbar
Signed-off-by: Evzen Gasta <evzen.ml@seznam.cz>
This commit is contained in:
parent
6be274d96e
commit
86ada91fcc
5 changed files with 119 additions and 316 deletions
|
|
@ -36,4 +36,10 @@ export type GoToInfo =
|
|||
| (PodInfo & { type: 'Pod' })
|
||||
| (ContainerInfo & { type: 'Container' })
|
||||
| (ImageInfo & { type: 'Image' })
|
||||
| (VolumeInfo & { type: 'Volume' });
|
||||
| (VolumeInfo & { type: 'Volume' })
|
||||
| (NavigationInfo & { type: 'Navigation' });
|
||||
|
||||
export interface NavigationInfo {
|
||||
name: string;
|
||||
link: string;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,12 +3,14 @@ import { faChevronRight, faMagnifyingGlass } from '@fortawesome/free-solid-svg-i
|
|||
import { Button, Input } from '@podman-desktop/ui-svelte';
|
||||
import { Icon } from '@podman-desktop/ui-svelte/icons';
|
||||
import { onMount, tick } from 'svelte';
|
||||
import { router } from 'tinro';
|
||||
|
||||
import { handleNavigation } from '/@/navigation';
|
||||
import { commandsInfos } from '/@/stores/commands';
|
||||
import { containersInfos } from '/@/stores/containers';
|
||||
import { context } from '/@/stores/context';
|
||||
import { imagesInfos } from '/@/stores/images';
|
||||
import { navigationRegistry, type NavigationRegistryEntry } from '/@/stores/navigation/navigation-registry';
|
||||
import { podsInfos } from '/@/stores/pods';
|
||||
import { volumeListInfos } from '/@/stores/volumes';
|
||||
import type { CommandInfo } from '/@api/command-info';
|
||||
|
|
@ -68,18 +70,16 @@ let searchOptions: SearchOption[] = $derived([
|
|||
{ text: 'Go to', shortCut: [`${modifierC}F`] },
|
||||
]);
|
||||
let searchOptionsSelectedIndex: number = $state(0);
|
||||
let imageItems: ImageInfo[] = $state([]);
|
||||
let containerItems: ContainerInfo[] = $state([]);
|
||||
let podItems: PodInfo[] = $state([]);
|
||||
let volumeItems: VolumeInfo[] = $state([]);
|
||||
|
||||
let documentationItems: DocumentationInfo[] = $state([]);
|
||||
let containerInfos: ContainerInfo[] = $derived($containersInfos);
|
||||
let podInfos: PodInfo[] = $derived($podsInfos);
|
||||
let volumInfos: VolumeInfo[] = $derived($volumeListInfos.map(info => info.Volumes).flat());
|
||||
let imageInfos: ImageInfo[] = $derived($imagesInfos);
|
||||
|
||||
let goToItems: GoToInfo[] = $derived(createGoToItems(imageInfos, containerInfos, podInfos, volumInfos));
|
||||
let navigationItems: NavigationRegistryEntry[] = $derived($navigationRegistry);
|
||||
let goToItems: GoToInfo[] = $derived(
|
||||
createGoToItems(imageInfos, containerInfos, podInfos, volumInfos, navigationItems),
|
||||
);
|
||||
|
||||
// Keep backward compatibility with existing variable name
|
||||
let filteredCommandInfoItems: CommandInfo[] = $derived(
|
||||
|
|
@ -278,6 +278,8 @@ async function executeAction(index: number): Promise<void> {
|
|||
page: NavigationPage.VOLUME,
|
||||
parameters: { name: item.Name, engineId: item.engineId },
|
||||
});
|
||||
} else if (item.type === 'Navigation') {
|
||||
router.goto(item.link);
|
||||
}
|
||||
} else {
|
||||
// Command item
|
||||
|
|
@ -403,7 +405,11 @@ function isDocItem(item: CommandInfo | DocumentationInfo | GoToInfo): item is Do
|
|||
{#if docItem}
|
||||
{(item.category)}: {(item.name)}
|
||||
{:else if goToItem}
|
||||
{(item.type)}: {(getGoToDisplayText(item))}
|
||||
{#if item.type === 'Navigation'}
|
||||
{item.name}
|
||||
{:else}
|
||||
{(item.type)}: {(getGoToDisplayText(item))}
|
||||
{/if}
|
||||
{:else}
|
||||
{(item.title)}
|
||||
{/if}
|
||||
|
|
|
|||
|
|
@ -16,8 +16,9 @@
|
|||
* SPDX-License-Identifier: Apache-2.0
|
||||
***********************************************************************/
|
||||
|
||||
import type { NavigationRegistryEntry } from '/@/stores/navigation/navigation-registry';
|
||||
import type { ContainerInfo } from '/@api/container-info';
|
||||
import type { GoToInfo } from '/@api/documentation-info';
|
||||
import type { GoToInfo, NavigationInfo } from '/@api/documentation-info';
|
||||
import type { ImageInfo } from '/@api/image-info';
|
||||
import type { PodInfo } from '/@api/pod-info';
|
||||
import type { VolumeInfo } from '/@api/volume-info';
|
||||
|
|
@ -42,16 +43,104 @@ export function getGoToDisplayText(goToInfo: GoToInfo): string {
|
|||
return goToInfo.Name;
|
||||
} else if (goToInfo.type === 'Volume') {
|
||||
return goToInfo.Name.substring(0, 12);
|
||||
} else if (goToInfo.type === 'Navigation') {
|
||||
return goToInfo.name;
|
||||
}
|
||||
return 'Unknown';
|
||||
}
|
||||
|
||||
// Helper function to extract and capitalize path prefix from link
|
||||
function extractPathPrefix(link: string, entryName: string): string | undefined {
|
||||
// Remove leading slash and split by '/'
|
||||
const pathSegments = link.replace(/^\//, '').split('/');
|
||||
|
||||
if (pathSegments.length === 0 || pathSegments[0] === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
const firstSegment = pathSegments[0];
|
||||
|
||||
// For submenu items (like Kubernetes Dashboard), use the parent category
|
||||
if (pathSegments.length > 1) {
|
||||
const capitalizedSegment = firstSegment.charAt(0).toUpperCase() + firstSegment.slice(1);
|
||||
return capitalizedSegment;
|
||||
}
|
||||
|
||||
// For main navigation items, don't add prefix if name matches path
|
||||
const capitalizedSegment = firstSegment.charAt(0).toUpperCase() + firstSegment.slice(1);
|
||||
if (entryName.toLowerCase() === firstSegment.toLowerCase()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Capitalize first letter and return
|
||||
return capitalizedSegment;
|
||||
}
|
||||
|
||||
// Helper function to process a single navigation entry
|
||||
function processNavigationEntry(entry: NavigationRegistryEntry, items: NavigationInfo[], parentName = ''): void {
|
||||
// Skip hidden entries
|
||||
if (entry.hidden) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Determine the display name with appropriate prefix and count
|
||||
let displayName = entry.name;
|
||||
|
||||
// Add count in parentheses if available
|
||||
const count = entry.counter || 0;
|
||||
const countSuffix = count > 0 ? ` (${count})` : '';
|
||||
|
||||
// Add prefix based on the entry type and parent context
|
||||
if (parentName) {
|
||||
// For submenu items, use the parent name as prefix
|
||||
displayName = `${parentName}: ${entry.name}${countSuffix}`;
|
||||
} else {
|
||||
// Extract prefix from the link path dynamically
|
||||
const pathPrefix = extractPathPrefix(entry.link, entry.name);
|
||||
if (pathPrefix) {
|
||||
displayName = `${pathPrefix}> ${entry.name}${countSuffix}`;
|
||||
} else {
|
||||
// No prefix needed, just add count
|
||||
displayName = `${entry.name}${countSuffix}`;
|
||||
}
|
||||
}
|
||||
|
||||
// Only add actual navigation entries (type 'entry'), not groups or submenus
|
||||
if (entry.type === 'entry') {
|
||||
const navigationInfo: NavigationInfo = {
|
||||
name: displayName,
|
||||
link: entry.link,
|
||||
};
|
||||
|
||||
items.push(navigationInfo);
|
||||
}
|
||||
|
||||
// Process submenu items if they exist
|
||||
if (entry.items && entry.items.length > 0) {
|
||||
entry.items.forEach(subItem => {
|
||||
processNavigationEntry(subItem, items, entry.name);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to extract navigation paths from navigation registry
|
||||
function extractNavigationPaths(entries: NavigationRegistryEntry[]): NavigationInfo[] {
|
||||
const items: NavigationInfo[] = [];
|
||||
|
||||
entries.forEach(entry => {
|
||||
processNavigationEntry(entry, items);
|
||||
});
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
// Helper function to create GoToInfo items from resources
|
||||
export function createGoToItems(
|
||||
images: ImageInfo[],
|
||||
containers: ContainerInfo[],
|
||||
pods: PodInfo[],
|
||||
volumes: VolumeInfo[],
|
||||
navigationEntries: NavigationRegistryEntry[] = [],
|
||||
): GoToInfo[] {
|
||||
const items: GoToInfo[] = [];
|
||||
|
||||
|
|
@ -75,5 +164,13 @@ export function createGoToItems(
|
|||
items.push({ type: 'Volume', ...volume });
|
||||
});
|
||||
|
||||
// Add navigation registry entries
|
||||
if (navigationEntries.length > 0) {
|
||||
const navigationInfos = extractNavigationPaths(navigationEntries);
|
||||
navigationInfos.forEach(navigationInfo => {
|
||||
items.push({ type: 'Navigation', ...navigationInfo });
|
||||
});
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,9 +27,7 @@ const config = {
|
|||
markdown: {
|
||||
mermaid: true,
|
||||
parseFrontMatter: async params => {
|
||||
// Only call the release notes parser per-file
|
||||
const result = await createNotesFiles(params);
|
||||
return result;
|
||||
return createNotesFiles(params);
|
||||
},
|
||||
},
|
||||
themes: ['@docusaurus/theme-mermaid'],
|
||||
|
|
|
|||
|
|
@ -1,304 +0,0 @@
|
|||
import { writeFile } from 'node:fs/promises';
|
||||
|
||||
import { beforeEach, describe, expect, test, vi } from 'vitest';
|
||||
|
||||
import { extractNavigationFromSidebar, generateJsonOverviewFile } from './sidebar-content-parser';
|
||||
|
||||
// Mock the writeFile function
|
||||
vi.mock('node:fs/promises', () => ({
|
||||
writeFile: vi.fn(),
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('extractNavigationFromSidebar', () => {
|
||||
test('should extract navigation items from simple doc items', () => {
|
||||
const sidebarItems = [
|
||||
{
|
||||
type: 'doc' as const,
|
||||
id: 'intro',
|
||||
label: 'Introduction',
|
||||
},
|
||||
{
|
||||
type: 'doc' as const,
|
||||
id: 'running-a-kubernetes-cluster',
|
||||
label: 'Running a Kubernetes cluster',
|
||||
},
|
||||
];
|
||||
|
||||
const result = extractNavigationFromSidebar(sidebarItems, 'https://podman-desktop.io', 'docs');
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
name: 'Introduction',
|
||||
url: 'https://podman-desktop.io/docs/intro',
|
||||
},
|
||||
{
|
||||
name: 'Running a kubernetes cluster',
|
||||
url: 'https://podman-desktop.io/docs/running-a-kubernetes-cluster',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('should handle items without labels by using id and formatting it', () => {
|
||||
const sidebarItems = [
|
||||
{
|
||||
type: 'doc' as const,
|
||||
id: 'deploying-a-kubernetes-application',
|
||||
},
|
||||
];
|
||||
|
||||
const result = extractNavigationFromSidebar(sidebarItems, 'https://podman-desktop.io', 'docs');
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
name: 'Deploying a kubernetes application',
|
||||
url: 'https://podman-desktop.io/docs/deploying-a-kubernetes-application',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('should handle tutorial index case correctly', () => {
|
||||
const sidebarItems = [
|
||||
{
|
||||
type: 'doc' as const,
|
||||
id: 'index',
|
||||
label: 'Introduction',
|
||||
},
|
||||
];
|
||||
|
||||
const result = extractNavigationFromSidebar(sidebarItems, 'https://podman-desktop.io', 'tutorial');
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
name: 'Introduction',
|
||||
url: 'https://podman-desktop.io/tutorial',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('should handle category items recursively', () => {
|
||||
const sidebarItems = [
|
||||
{
|
||||
type: 'category' as const,
|
||||
label: 'Getting Started',
|
||||
items: [
|
||||
{
|
||||
type: 'doc' as const,
|
||||
id: 'intro',
|
||||
label: 'Introduction',
|
||||
},
|
||||
{
|
||||
type: 'doc' as const,
|
||||
id: 'installation',
|
||||
label: 'Installation',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const result = extractNavigationFromSidebar(sidebarItems, 'https://podman-desktop.io', 'docs');
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
name: 'Introduction',
|
||||
url: 'https://podman-desktop.io/docs/intro',
|
||||
},
|
||||
{
|
||||
name: 'Installation',
|
||||
url: 'https://podman-desktop.io/docs/installation',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('should format names correctly by replacing hyphens with spaces and capitalizing first letter', () => {
|
||||
const sidebarItems = [
|
||||
{
|
||||
type: 'doc' as const,
|
||||
id: 'getting-started-with-compose',
|
||||
label: 'Getting started with Compose',
|
||||
},
|
||||
{
|
||||
type: 'doc' as const,
|
||||
id: 'running-an-ai-application',
|
||||
},
|
||||
];
|
||||
|
||||
const result = extractNavigationFromSidebar(sidebarItems, 'https://podman-desktop.io', 'docs');
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
name: 'Getting started with compose',
|
||||
url: 'https://podman-desktop.io/docs/getting-started-with-compose',
|
||||
},
|
||||
{
|
||||
name: 'Running an AI application',
|
||||
url: 'https://podman-desktop.io/docs/running-an-ai-application',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('should keep AI capitalized in names', () => {
|
||||
const sidebarItems = [
|
||||
{
|
||||
type: 'doc' as const,
|
||||
id: 'ai-lab-start-recipe',
|
||||
label: 'AI Lab Start Recipe',
|
||||
},
|
||||
{
|
||||
type: 'doc' as const,
|
||||
id: 'running-an-ai-application',
|
||||
},
|
||||
{
|
||||
type: 'doc' as const,
|
||||
id: 'ai-powered-tools',
|
||||
},
|
||||
];
|
||||
|
||||
const result = extractNavigationFromSidebar(sidebarItems, 'https://podman-desktop.io', 'docs');
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
name: 'AI lab start recipe',
|
||||
url: 'https://podman-desktop.io/docs/ai-lab-start-recipe',
|
||||
},
|
||||
{
|
||||
name: 'Running an AI application',
|
||||
url: 'https://podman-desktop.io/docs/running-an-ai-application',
|
||||
},
|
||||
{
|
||||
name: 'AI powered tools',
|
||||
url: 'https://podman-desktop.io/docs/ai-powered-tools',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('should skip items with empty names', () => {
|
||||
const sidebarItems = [
|
||||
{
|
||||
type: 'doc' as const,
|
||||
id: '',
|
||||
label: '',
|
||||
},
|
||||
{
|
||||
type: 'doc' as const,
|
||||
id: 'valid-item',
|
||||
label: 'Valid Item',
|
||||
},
|
||||
];
|
||||
|
||||
const result = extractNavigationFromSidebar(sidebarItems, 'https://podman-desktop.io', 'docs');
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
name: 'Valid item',
|
||||
url: 'https://podman-desktop.io/docs/valid-item',
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('generateJsonOverviewFile', () => {
|
||||
test('should generate JSON file for docs section', async () => {
|
||||
const sidebarItems = [
|
||||
{
|
||||
type: 'doc' as const,
|
||||
id: 'intro',
|
||||
label: 'Introduction',
|
||||
},
|
||||
];
|
||||
|
||||
await generateJsonOverviewFile(sidebarItems, 'docs', 'https://podman-desktop.io', './static/docs.json');
|
||||
|
||||
expect(writeFile).toHaveBeenCalledWith(
|
||||
'./static/docs.json',
|
||||
JSON.stringify([
|
||||
{
|
||||
name: 'Introduction',
|
||||
url: 'https://podman-desktop.io/docs/intro',
|
||||
},
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
test('should generate JSON file for tutorial section', async () => {
|
||||
const sidebarItems = [
|
||||
{
|
||||
type: 'doc' as const,
|
||||
id: 'index',
|
||||
label: 'Introduction',
|
||||
},
|
||||
];
|
||||
|
||||
await generateJsonOverviewFile(sidebarItems, 'tutorial', 'https://podman-desktop.io', './static/tutorials.json');
|
||||
|
||||
expect(writeFile).toHaveBeenCalledWith(
|
||||
'./static/tutorials.json',
|
||||
JSON.stringify([
|
||||
{
|
||||
name: 'Introduction',
|
||||
url: 'https://podman-desktop.io/tutorial',
|
||||
},
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
test('should sort items alphabetically by name', async () => {
|
||||
const sidebarItems = [
|
||||
{
|
||||
type: 'doc' as const,
|
||||
id: 'zebra',
|
||||
label: 'Zebra',
|
||||
},
|
||||
{
|
||||
type: 'doc' as const,
|
||||
id: 'apple',
|
||||
label: 'Apple',
|
||||
},
|
||||
{
|
||||
type: 'doc' as const,
|
||||
id: 'banana',
|
||||
label: 'Banana',
|
||||
},
|
||||
];
|
||||
|
||||
await generateJsonOverviewFile(sidebarItems, 'docs', 'https://podman-desktop.io', './static/docs.json');
|
||||
|
||||
const expectedContent = JSON.stringify([
|
||||
{
|
||||
name: 'Apple',
|
||||
url: 'https://podman-desktop.io/docs/apple',
|
||||
},
|
||||
{
|
||||
name: 'Banana',
|
||||
url: 'https://podman-desktop.io/docs/banana',
|
||||
},
|
||||
{
|
||||
name: 'Zebra',
|
||||
url: 'https://podman-desktop.io/docs/zebra',
|
||||
},
|
||||
]);
|
||||
|
||||
expect(writeFile).toHaveBeenCalledWith('./static/docs.json', expectedContent);
|
||||
});
|
||||
|
||||
test('should propagate writeFile errors', async () => {
|
||||
const mockError = new Error('Write failed');
|
||||
vi.mocked(writeFile).mockRejectedValueOnce(mockError);
|
||||
|
||||
const sidebarItems = [
|
||||
{
|
||||
type: 'doc' as const,
|
||||
id: 'intro',
|
||||
label: 'Introduction',
|
||||
},
|
||||
];
|
||||
|
||||
// Should throw the error
|
||||
await expect(
|
||||
generateJsonOverviewFile(sidebarItems, 'docs', 'https://podman-desktop.io', './static/docs.json'),
|
||||
).rejects.toThrow('Write failed');
|
||||
});
|
||||
});
|
||||
Loading…
Reference in a new issue