Skip to content

Commit d069371

Browse files
committed
add more components
1 parent 213c62d commit d069371

File tree

16 files changed

+1017
-2
lines changed

16 files changed

+1017
-2
lines changed
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import type { Meta, StoryObj } from '@storybook/react-vite'
2+
3+
import { ModelName } from './ModelName'
4+
5+
const meta: Meta<typeof ModelName> = {
6+
title: 'Components/ModelName',
7+
component: ModelName,
8+
tags: ['autodocs'],
9+
argTypes: {
10+
name: { control: 'text' },
11+
hideCatalog: { control: 'boolean' },
12+
hideSchema: { control: 'boolean' },
13+
hideIcon: { control: 'boolean' },
14+
showTooltip: { control: 'boolean' },
15+
className: { control: 'text' },
16+
},
17+
}
18+
19+
export default meta
20+
type Story = StoryObj<typeof ModelName>
21+
22+
export const Default: Story = {
23+
args: {
24+
name: 'catalog.schema.model',
25+
},
26+
}
27+
28+
export const WithoutCatalog: Story = {
29+
args: {
30+
name: 'catalog.schema.model',
31+
hideCatalog: true,
32+
},
33+
}
34+
35+
export const WithoutSchema: Story = {
36+
args: {
37+
name: 'catalog.schema.model',
38+
hideSchema: true,
39+
},
40+
}
41+
42+
export const WithoutIcon: Story = {
43+
args: {
44+
name: 'catalog.schema.model',
45+
hideIcon: true,
46+
},
47+
}
48+
49+
export const WithTooltip: Story = {
50+
args: {
51+
name: 'catalog.schema.model',
52+
hideCatalog: true,
53+
hideSchema: true,
54+
showTooltip: true,
55+
},
56+
}
57+
58+
export const WithoutTooltip: Story = {
59+
args: {
60+
name: 'catalog.schema.model',
61+
showTooltip: false,
62+
},
63+
}
64+
65+
export const CustomClassName: Story = {
66+
args: {
67+
name: 'catalog.schema.model',
68+
className: 'text-xl font-bold',
69+
},
70+
}
71+
72+
export const LongName: Story = {
73+
args: {
74+
name: 'veryveryverylongcatalogname.veryveryverylongschamename.veryveryverylongmodelnameveryveryverylongmodelname',
75+
},
76+
}
77+
78+
export const Grayscale: Story = {
79+
args: {
80+
name: 'catalog.schema.model',
81+
grayscale: true,
82+
},
83+
}
84+
85+
export const Link: Story = {
86+
args: {
87+
name: 'catalog.schema.model',
88+
link: 'https://www.google.com',
89+
grayscale: true,
90+
showCopy: true,
91+
},
92+
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import userEvent from '@testing-library/user-event'
2+
import { describe, expect, it, vi } from 'vitest'
3+
import { render, screen, within } from '@testing-library/react'
4+
import { ModelName } from './ModelName'
5+
6+
describe('ModelName', () => {
7+
it('renders full model name with catalog, schema, and model', () => {
8+
render(<ModelName name="cat.sch.model" />)
9+
expect(screen.getByText('cat')).toBeInTheDocument()
10+
expect(screen.getByText('sch')).toBeInTheDocument()
11+
expect(screen.getByText('model')).toBeInTheDocument()
12+
})
13+
14+
it('hides catalog when hideCatalog is true', () => {
15+
render(
16+
<ModelName
17+
name="cat.sch.model"
18+
hideCatalog
19+
/>,
20+
)
21+
expect(screen.queryByText('cat')).not.toBeInTheDocument()
22+
expect(screen.getByText('sch')).toBeInTheDocument()
23+
expect(screen.getByText('model')).toBeInTheDocument()
24+
})
25+
26+
it('hides schema when hideSchema is true', () => {
27+
render(
28+
<ModelName
29+
name="cat.sch.model"
30+
hideSchema
31+
/>,
32+
)
33+
expect(screen.getByText('cat')).toBeInTheDocument()
34+
expect(screen.queryByText('sch')).not.toBeInTheDocument()
35+
expect(screen.getByText('model')).toBeInTheDocument()
36+
})
37+
38+
it('hides icon when hideIcon is true', () => {
39+
const { container } = render(
40+
<ModelName
41+
name="cat.sch.model"
42+
hideIcon
43+
/>,
44+
)
45+
// Should not render the Box icon SVG
46+
expect(container.querySelector('svg')).toBeNull()
47+
})
48+
49+
it('shows tooltip when showTooltip is true and catalog or schema is hidden', async () => {
50+
render(
51+
<ModelName
52+
name="cat.sch.model"
53+
hideCatalog
54+
showTooltip
55+
/>,
56+
)
57+
// Tooltip trigger is present (icon)
58+
const modelName = screen.getByTestId('model-name')
59+
expect(modelName).toBeInTheDocument()
60+
await userEvent.hover(modelName)
61+
const tooltip = await screen.findByRole('tooltip')
62+
expect(tooltip).toBeInTheDocument()
63+
within(tooltip).getByText('cat.sch.model')
64+
})
65+
66+
it('throws error if name is empty', () => {
67+
// Suppress error output for this test
68+
const spy = vi.spyOn(console, 'error').mockImplementation(() => {})
69+
expect(() => render(<ModelName name="" />)).toThrow()
70+
spy.mockRestore()
71+
})
72+
})
Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
import { Box, Check, Copy } from 'lucide-react'
2+
import { useMemo } from 'react'
3+
4+
import { ClipboardCopy } from '@/components/ClipboardCopy/ClipboardCopy'
5+
import { cn } from '@/utils'
6+
import { EnumSize } from '@/types/enums'
7+
import { isNilOrEmptyString, truncate } from '@/utils'
8+
import Tooltip from '@/components/Tooltip/Tooltip'
9+
10+
const MODEL_NAME_TOOLTIP_SIDE_OFFSET = 6
11+
const MODEL_NAME_ICON_SIZE = 16
12+
13+
export function ModelName({
14+
name,
15+
hideCatalog = false,
16+
hideSchema = false,
17+
hideIcon = false,
18+
showTooltip = false,
19+
showCopy = false,
20+
truncateMaxChars = 25,
21+
truncateLimitBefore = 5,
22+
truncateLimitAfter = 7,
23+
grayscale = false,
24+
link,
25+
className,
26+
}: {
27+
name: string
28+
hideCatalog?: boolean
29+
hideSchema?: boolean
30+
hideIcon?: boolean
31+
showTooltip?: boolean
32+
showCopy?: boolean
33+
truncateMaxChars?: number
34+
truncateLimitBefore?: number
35+
truncateLimitAfter?: number
36+
grayscale?: boolean
37+
link?: string
38+
className?: string
39+
}) {
40+
if (isNilOrEmptyString(name))
41+
throw new Error('Model name should not be empty')
42+
43+
const truncateMaxCharsModel = truncateMaxChars * 2
44+
45+
const { catalog, schema, model, withTooltip } = useMemo(() => {
46+
const [model, schema, catalog] = name.split('.').reverse()
47+
48+
return {
49+
catalog: hideCatalog ? undefined : catalog,
50+
schema: hideSchema ? undefined : schema,
51+
model,
52+
withTooltip:
53+
((hideCatalog && catalog) ||
54+
(hideSchema && schema) ||
55+
[catalog, schema].some(v => v && v.length > truncateMaxChars) ||
56+
model.length > truncateMaxCharsModel) &&
57+
showTooltip,
58+
}
59+
}, [
60+
name,
61+
hideCatalog,
62+
hideSchema,
63+
truncateMaxCharsModel,
64+
showTooltip,
65+
truncateMaxChars,
66+
])
67+
68+
function renderTooltip() {
69+
return (
70+
<Tooltip
71+
trigger={renderName()}
72+
sideOffset={MODEL_NAME_TOOLTIP_SIDE_OFFSET}
73+
side="top"
74+
className="text-xs px-2 py-1 rounded-sm font-semibold"
75+
>
76+
{name}
77+
</Tooltip>
78+
)
79+
}
80+
81+
function renderIcon() {
82+
return (
83+
<Box
84+
size={MODEL_NAME_ICON_SIZE}
85+
className={cn(
86+
'mr-1 flex-shrink-0',
87+
grayscale
88+
? 'text-model-name-grayscale-model'
89+
: 'text-model-name-model',
90+
link && '-mt-[4px]',
91+
)}
92+
/>
93+
)
94+
}
95+
96+
console.assert(model, 'Model name should not be empty')
97+
98+
function renderName() {
99+
return (
100+
<span
101+
data-testid="model-name"
102+
className="overflow-hidden"
103+
>
104+
{catalog && (
105+
<>
106+
<span
107+
className={cn(
108+
grayscale
109+
? 'text-model-name-grayscale-catalog'
110+
: 'text-model-name-catalog',
111+
)}
112+
>
113+
{_truncate(catalog)}
114+
</span>
115+
.
116+
</>
117+
)}
118+
{schema && (
119+
<>
120+
<span
121+
className={cn(
122+
grayscale
123+
? 'text-model-name-grayscale-schema'
124+
: 'text-model-name-schema',
125+
)}
126+
>
127+
{_truncate(schema)}
128+
</span>
129+
.
130+
</>
131+
)}
132+
<span
133+
className={cn(
134+
grayscale
135+
? 'text-model-name-grayscale-model'
136+
: 'text-model-name-model',
137+
)}
138+
>
139+
{truncate(model, truncateMaxCharsModel, 15)}
140+
</span>
141+
</span>
142+
)
143+
}
144+
145+
function renderNameWithTooltip() {
146+
return withTooltip ? renderTooltip() : renderName()
147+
}
148+
149+
function _truncate(name: string, maxChars: number = truncateMaxChars) {
150+
return truncate(
151+
name,
152+
maxChars,
153+
truncateLimitBefore,
154+
'...',
155+
truncateLimitAfter,
156+
)
157+
}
158+
159+
return (
160+
<span
161+
data-component="ModelName"
162+
className={cn(
163+
'inline-flex items-center whitespace-nowrap overflow-hidden font-semibold',
164+
className,
165+
)}
166+
>
167+
{!hideIcon && renderIcon()}
168+
{link ? (
169+
<a
170+
href={link}
171+
className={cn(
172+
'flex cursor-pointer border-b -mt-0.5 text-inherit',
173+
grayscale
174+
? 'border-model-name-grayscale-link hover:border-model-name-grayscale-link-hover'
175+
: 'border-model-name-link hover:border-model-name-link-hover',
176+
)}
177+
>
178+
{renderNameWithTooltip()}
179+
</a>
180+
) : (
181+
renderNameWithTooltip()
182+
)}
183+
{showCopy && (
184+
<ClipboardCopy
185+
size={EnumSize.XXS}
186+
text={name}
187+
className="ml-2 w-6 hover:text-model-name-copy-icon-hover active:text-model-name-copy-icon-hover"
188+
>
189+
{copied =>
190+
copied ? (
191+
<Check
192+
size={MODEL_NAME_ICON_SIZE}
193+
className="text-model-name-copy-icon"
194+
/>
195+
) : (
196+
<Copy
197+
size={MODEL_NAME_ICON_SIZE}
198+
className="text-model-name-copy-icon"
199+
/>
200+
)
201+
}
202+
</ClipboardCopy>
203+
)}
204+
</span>
205+
)
206+
}

0 commit comments

Comments
 (0)