Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .storybook/global.css
Original file line number Diff line number Diff line change
Expand Up @@ -81,3 +81,7 @@ h6 {
* {
font-family: "Mulish", "Helvetica Neue", Helvetica, Arial, sans-serif;
}

.custom-versions div a {
text-decoration: underline;
}
9 changes: 9 additions & 0 deletions src/components/Breadcrumb/Breadcrumb.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
font-size: 18px;
height: 32px;
justify-content: space-between;
gap: 10px;
min-height: 32px;
padding-left: 20px;
padding-right: 10px;
Expand Down Expand Up @@ -46,3 +47,11 @@
}
}
}

.versions {
margin-left: auto;

[aria-current] {
font-weight: bold;
}
}
49 changes: 49 additions & 0 deletions src/components/Breadcrumb/Breadcrumb.stories.tsx
Original file line number Diff line number Diff line change
@@ -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<typeof Breadcrumb> = {
component: Breadcrumb,
}
export default meta
type Story = StoryObj<typeof Breadcrumb>;
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 (
<ConfigProvider value={config}>
<Breadcrumb {...args}>
<input type='text' placeholder="Search..." />
</Breadcrumb>
</ConfigProvider>
)
},
}
27 changes: 27 additions & 0 deletions src/components/Breadcrumb/Breadcrumb.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(<ConfigProvider value={config}>
<Breadcrumb source={source} />
</ConfigProvider>)

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')
})
})
20 changes: 20 additions & 0 deletions src/components/Breadcrumb/Breadcrumb.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,32 @@ 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 {
source: Source,
children?: ReactNode
}

function Versions({ source }: { source: Source }) {
const { routes, customClass } = useConfig()

if (!source.versions) return null
const { label, versions } = source.versions

return <Dropdown label={label} className={cn(styles.versions, customClass?.versions)} align="right">
{versions.map(({ label, sourceId }) => {
return <a
key={sourceId}
role="menuitem"
href={routes?.getSourceRouteUrl?.({ sourceId })}
aria-current={sourceId === source.sourceId ? 'true' : undefined}
>{label}</a>
})}
</Dropdown>
}

/**
* Breadcrumb navigation
*/
Expand All @@ -21,6 +40,7 @@ export default function Breadcrumb({ source, children }: BreadcrumbProps) {
<a href={routes?.getSourceRouteUrl?.({ sourceId: part.sourceId }) ?? ''} key={depth}>{part.text}</a>
)}
</div>
{source.versions && <Versions source={source} />}
{children}
</nav>
}
1 change: 1 addition & 0 deletions src/hooks/useConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export interface Config {
slidePanel?: string
spinner?: string
textView?: string
versions?: string
welcome?: string
}
routes?: {
Expand Down
11 changes: 11 additions & 0 deletions src/lib/sources/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down