feat: added entries from navigation to searchbar

Signed-off-by: Evzen Gasta <evzen.ml@seznam.cz>
This commit is contained in:
Evzen Gasta 2025-10-03 06:59:36 +02:00 committed by Evžen Gasta
parent 6be274d96e
commit 86ada91fcc
5 changed files with 119 additions and 316 deletions

View file

@ -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;
}

View file

@ -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}

View file

@ -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;
}

View file

@ -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'],

View file

@ -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');
});
});