diff --git a/.storybook/global.css b/.storybook/global.css index cb50d95a..4fa8237a 100644 --- a/.storybook/global.css +++ b/.storybook/global.css @@ -81,3 +81,7 @@ h6 { * { font-family: "Mulish", "Helvetica Neue", Helvetica, Arial, sans-serif; } + +.custom-versions div a { + text-decoration: underline; +} diff --git a/src/components/Breadcrumb/Breadcrumb.module.css b/src/components/Breadcrumb/Breadcrumb.module.css index 614500f8..97adc781 100644 --- a/src/components/Breadcrumb/Breadcrumb.module.css +++ b/src/components/Breadcrumb/Breadcrumb.module.css @@ -5,6 +5,7 @@ font-size: 18px; height: 32px; justify-content: space-between; + gap: 10px; min-height: 32px; padding-left: 20px; padding-right: 10px; @@ -46,3 +47,11 @@ } } } + +.versions { + margin-left: auto; + + [aria-current] { + font-weight: bold; + } +} diff --git a/src/components/Breadcrumb/Breadcrumb.stories.tsx b/src/components/Breadcrumb/Breadcrumb.stories.tsx new file mode 100644 index 00000000..5cdf50c1 --- /dev/null +++ b/src/components/Breadcrumb/Breadcrumb.stories.tsx @@ -0,0 +1,49 @@ +import type { Meta, StoryObj } from '@storybook/react' +import { ConfigProvider } from '../../hooks/useConfig.js' +import Breadcrumb from './Breadcrumb.js' + +const meta: Meta = { + component: Breadcrumb, +} +export default meta +type Story = StoryObj; +export const Default: Story = { + args: { + source: { + kind: 'file', + sourceId: '/part1/part2/file.txt', + fileName: 'file.txt', + resolveUrl: '/part1/part2/file.txt', + sourceParts: [ + { text: '/', sourceId: '/' }, + { text: 'part1/', sourceId: '/part1/' }, + { text: 'part2/', sourceId: '/part1/part2/' }, + ], + versions: { + label: 'Branches', + versions: [ + { label: 'master', sourceId: '/part1/part2/file.txt' }, + { label: 'dev', sourceId: '/part1/part2/file.txt?branch=dev' }, + { label: 'refs/convert/parquet', sourceId: '/part1/part2/file.txt?branch=refs/convert/parquet' }, + ], + }, + }, + }, + render: (args) => { + const config = { + routes: { + getSourceRouteUrl: ({ sourceId }: { sourceId: string }) => `/files?key=${sourceId}`, + }, + customClass: { + versions: 'custom-versions', + }, + } + return ( + + + + + + ) + }, +} diff --git a/src/components/Breadcrumb/Breadcrumb.test.tsx b/src/components/Breadcrumb/Breadcrumb.test.tsx index ecf6c7de..9723af52 100644 --- a/src/components/Breadcrumb/Breadcrumb.test.tsx +++ b/src/components/Breadcrumb/Breadcrumb.test.tsx @@ -27,4 +27,31 @@ describe('Breadcrumb Component', () => { const subdir2Link = getByText('subdir2/') expect(subdir2Link.closest('a')?.getAttribute('href')).toBe('/files?key=subdir1/subdir2/') }) + + it('handles versions correctly', () => { + const source = getHyperparamSource('subdir1/subdir2/', { endpoint }) + assert(source !== undefined) + source.versions = { + label: 'Versions', + versions: [ + { label: 'v1.0', sourceId: 'v1.0' }, + { label: 'v2.0', sourceId: 'v2.0' }, + ], + } + + const config: Config = { + routes: { + getSourceRouteUrl: ({ sourceId }) => `/files?key=${sourceId}`, + }, + } + const { getByText, getAllByRole } = render( + + ) + + const versionsLabel = getByText('Versions') + expect(versionsLabel).toBeDefined() + const versionLinks = getAllByRole('menuitem') + expect(versionLinks.length).toBe(2) + expect(versionLinks[0]?.getAttribute('href')).toBe('/files?key=v1.0') + }) }) diff --git a/src/components/Breadcrumb/Breadcrumb.tsx b/src/components/Breadcrumb/Breadcrumb.tsx index 90db82db..60c87350 100644 --- a/src/components/Breadcrumb/Breadcrumb.tsx +++ b/src/components/Breadcrumb/Breadcrumb.tsx @@ -2,6 +2,7 @@ import type { ReactNode } from 'react' import { useConfig } from '../../hooks/useConfig.js' import type { Source } from '../../lib/sources/types.js' import { cn } from '../../lib/utils.js' +import Dropdown from '../Dropdown/Dropdown.js' import styles from './Breadcrumb.module.css' interface BreadcrumbProps { @@ -9,6 +10,24 @@ interface BreadcrumbProps { children?: ReactNode } +function Versions({ source }: { source: Source }) { + const { routes, customClass } = useConfig() + + if (!source.versions) return null + const { label, versions } = source.versions + + return + {versions.map(({ label, sourceId }) => { + return {label} + })} + +} + /** * Breadcrumb navigation */ @@ -21,6 +40,7 @@ export default function Breadcrumb({ source, children }: BreadcrumbProps) { {part.text} )} + {source.versions && } {children} } diff --git a/src/hooks/useConfig.ts b/src/hooks/useConfig.ts index 6cb6d4c2..22c27636 100644 --- a/src/hooks/useConfig.ts +++ b/src/hooks/useConfig.ts @@ -27,6 +27,7 @@ export interface Config { slidePanel?: string spinner?: string textView?: string + versions?: string welcome?: string } routes?: { diff --git a/src/lib/sources/types.ts b/src/lib/sources/types.ts index c3ccafec..1d342348 100644 --- a/src/lib/sources/types.ts +++ b/src/lib/sources/types.ts @@ -14,9 +14,20 @@ export interface SourcePart { sourceId: string } +export interface Version { + label: string + sourceId: string +} + +export interface VersionsData { + label: string // "version" or "branch" + versions: Version[] +} + interface BaseSource { sourceId: string sourceParts: SourcePart[] + versions?: VersionsData } export interface FileSource extends BaseSource {