Skip to content

Commit ccb1940

Browse files
authored
Add Versions component in Breadcrumb (#224)
* Add Versions component in Breadcrumb Can be used for iceberg versions, or for git branches, for example. See hyparam/space#3 * Fix styles
1 parent 977df82 commit ccb1940

File tree

7 files changed

+121
-0
lines changed

7 files changed

+121
-0
lines changed

.storybook/global.css

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,3 +81,7 @@ h6 {
8181
* {
8282
font-family: "Mulish", "Helvetica Neue", Helvetica, Arial, sans-serif;
8383
}
84+
85+
.custom-versions div a {
86+
text-decoration: underline;
87+
}

src/components/Breadcrumb/Breadcrumb.module.css

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
font-size: 18px;
66
height: 32px;
77
justify-content: space-between;
8+
gap: 10px;
89
min-height: 32px;
910
padding-left: 20px;
1011
padding-right: 10px;
@@ -46,3 +47,11 @@
4647
}
4748
}
4849
}
50+
51+
.versions {
52+
margin-left: auto;
53+
54+
[aria-current] {
55+
font-weight: bold;
56+
}
57+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import type { Meta, StoryObj } from '@storybook/react'
2+
import { ConfigProvider } from '../../hooks/useConfig.js'
3+
import Breadcrumb from './Breadcrumb.js'
4+
5+
const meta: Meta<typeof Breadcrumb> = {
6+
component: Breadcrumb,
7+
}
8+
export default meta
9+
type Story = StoryObj<typeof Breadcrumb>;
10+
export const Default: Story = {
11+
args: {
12+
source: {
13+
kind: 'file',
14+
sourceId: '/part1/part2/file.txt',
15+
fileName: 'file.txt',
16+
resolveUrl: '/part1/part2/file.txt',
17+
sourceParts: [
18+
{ text: '/', sourceId: '/' },
19+
{ text: 'part1/', sourceId: '/part1/' },
20+
{ text: 'part2/', sourceId: '/part1/part2/' },
21+
],
22+
versions: {
23+
label: 'Branches',
24+
versions: [
25+
{ label: 'master', sourceId: '/part1/part2/file.txt' },
26+
{ label: 'dev', sourceId: '/part1/part2/file.txt?branch=dev' },
27+
{ label: 'refs/convert/parquet', sourceId: '/part1/part2/file.txt?branch=refs/convert/parquet' },
28+
],
29+
},
30+
},
31+
},
32+
render: (args) => {
33+
const config = {
34+
routes: {
35+
getSourceRouteUrl: ({ sourceId }: { sourceId: string }) => `/files?key=${sourceId}`,
36+
},
37+
customClass: {
38+
versions: 'custom-versions',
39+
},
40+
}
41+
return (
42+
<ConfigProvider value={config}>
43+
<Breadcrumb {...args}>
44+
<input type='text' placeholder="Search..." />
45+
</Breadcrumb>
46+
</ConfigProvider>
47+
)
48+
},
49+
}

src/components/Breadcrumb/Breadcrumb.test.tsx

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,4 +27,31 @@ describe('Breadcrumb Component', () => {
2727
const subdir2Link = getByText('subdir2/')
2828
expect(subdir2Link.closest('a')?.getAttribute('href')).toBe('/files?key=subdir1/subdir2/')
2929
})
30+
31+
it('handles versions correctly', () => {
32+
const source = getHyperparamSource('subdir1/subdir2/', { endpoint })
33+
assert(source !== undefined)
34+
source.versions = {
35+
label: 'Versions',
36+
versions: [
37+
{ label: 'v1.0', sourceId: 'v1.0' },
38+
{ label: 'v2.0', sourceId: 'v2.0' },
39+
],
40+
}
41+
42+
const config: Config = {
43+
routes: {
44+
getSourceRouteUrl: ({ sourceId }) => `/files?key=${sourceId}`,
45+
},
46+
}
47+
const { getByText, getAllByRole } = render(<ConfigProvider value={config}>
48+
<Breadcrumb source={source} />
49+
</ConfigProvider>)
50+
51+
const versionsLabel = getByText('Versions')
52+
expect(versionsLabel).toBeDefined()
53+
const versionLinks = getAllByRole('menuitem')
54+
expect(versionLinks.length).toBe(2)
55+
expect(versionLinks[0]?.getAttribute('href')).toBe('/files?key=v1.0')
56+
})
3057
})

src/components/Breadcrumb/Breadcrumb.tsx

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,32 @@ import type { ReactNode } from 'react'
22
import { useConfig } from '../../hooks/useConfig.js'
33
import type { Source } from '../../lib/sources/types.js'
44
import { cn } from '../../lib/utils.js'
5+
import Dropdown from '../Dropdown/Dropdown.js'
56
import styles from './Breadcrumb.module.css'
67

78
interface BreadcrumbProps {
89
source: Source,
910
children?: ReactNode
1011
}
1112

13+
function Versions({ source }: { source: Source }) {
14+
const { routes, customClass } = useConfig()
15+
16+
if (!source.versions) return null
17+
const { label, versions } = source.versions
18+
19+
return <Dropdown label={label} className={cn(styles.versions, customClass?.versions)} align="right">
20+
{versions.map(({ label, sourceId }) => {
21+
return <a
22+
key={sourceId}
23+
role="menuitem"
24+
href={routes?.getSourceRouteUrl?.({ sourceId })}
25+
aria-current={sourceId === source.sourceId ? 'true' : undefined}
26+
>{label}</a>
27+
})}
28+
</Dropdown>
29+
}
30+
1231
/**
1332
* Breadcrumb navigation
1433
*/
@@ -21,6 +40,7 @@ export default function Breadcrumb({ source, children }: BreadcrumbProps) {
2140
<a href={routes?.getSourceRouteUrl?.({ sourceId: part.sourceId }) ?? ''} key={depth}>{part.text}</a>
2241
)}
2342
</div>
43+
{source.versions && <Versions source={source} />}
2444
{children}
2545
</nav>
2646
}

src/hooks/useConfig.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ export interface Config {
2727
slidePanel?: string
2828
spinner?: string
2929
textView?: string
30+
versions?: string
3031
welcome?: string
3132
}
3233
routes?: {

src/lib/sources/types.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,20 @@ export interface SourcePart {
1414
sourceId: string
1515
}
1616

17+
export interface Version {
18+
label: string
19+
sourceId: string
20+
}
21+
22+
export interface VersionsData {
23+
label: string // "version" or "branch"
24+
versions: Version[]
25+
}
26+
1727
interface BaseSource {
1828
sourceId: string
1929
sourceParts: SourcePart[]
30+
versions?: VersionsData
2031
}
2132

2233
export interface FileSource extends BaseSource {

0 commit comments

Comments
 (0)